Betrifft Oracle9i/10g Table Compression Autor Peter Hulm ([email protected]) Art der Info Technische Background Info (Januar 2004) Quelle Aus unserer Schulungs- und Beratungstätigkeit Einleitung Wer kennt sie nicht, die diversen Werkzeuge zur Datenkomprimierung. So gehören Winzip, Gzip, compress und Co mittlerweile zum Repertoire eines jeden PC-Anwenders. Dokumente, Mailanhänge, ja sogar ganze Filesysteme werden häufig „gezippt“. Aber wie sieht es eigentlich mit unseren Datenbanktabellen aus? Wäre es nicht auch angebracht, gerade diese, häufig sehr großen, Datenbestände zu komprimieren? Die folgenden Ausführungen skizzieren die Funktionsweise sowie die daraus resultierenden Einsatzmöglichkeiten der Datensegment-Komprimierung ab Oracle9i Release 2. Positionierung Das Konzept der Tabellenkomprimierung zielt speziell auf Data Warehouse Umgebungen und gehört dabei zu den Performance steigernden Präparaten aus der Oracle-Feature Apotheke. Verfügbar ist die Funktionalität ab Oracle Version 9 im Rahmen der Enterprise Edition. Gerade im DWH Umfeld existieren oft sehr große Datensegmente und komplexe Abfragestrukturen, deren Performance wesentlich vom I/O Durchsatz abhängt. Wiederholt höre ich von unseren Kunden, Speicherplatz ist heutzutage genügend vorhanden und im Vergleich zu anderen Systemressourcen verhältnismäßig schnell und günstig nachzurüsten. Wozu also dennoch komprimieren, wenn jeder Komprimierungs-Algorithmus auch noch zusätzliche CPU-Kapazitäten bindet? Hierzu ist folgendes anzumerken: 1. In Data Warehouse Umgebungen werden Daten häufig nach unterschiedlichsten Kriterien ausgewertet und verdichtet. Dabei müssen oftmals große Datensegmente von der Harddisk in den Hauptspeicher gelesen werden. Die Plattenkapazitäten an sich sind in diesem Zusammenhang sekundär. Der I/O Durchsatz aber ist für die Performance entscheidend! 10 Gigabyte mehr oder weniger physical I/O sind hier durchaus relevant. Gerade Datenbankanwendungen, die Oracle nicht nur als „Endlager“ für ihre Daten nutzen, sondern die gespeicherten Informationen weiterverarbeiten, profitieren von der Kompression. 2. Der notwendige CPU-Overhead jeder Komprimierung wird sehr stark vom jeweiligen Komprimieralgorithmus bestimmt. Das „gzip -9“ Kommando benötigt beispielsweise ein Vielfaches der CPU-Zeit eines „gzip -1“, liefert allerdings eine wesentlich bessere Kompressionsrate. Die Oracle Komprimierung benötigt Architektur-bedingt sehr wenig zusätzliche CPU-Leistung. Konzept / Architektur Als nächstes richten wir unser Augenmerk auf die zugrunde liegende Architektur. Oracle verwendet keinen klassischen Komprimieralgorithmus, sondern verdichtet die Daten auf logischer Ebene. Die Komprimierung erfolgt innerhalb der einzelnen Datenbankblöcke. Das Prinzip gestaltet sich dabei sehr einfach. Alle redundanten Attributwerte werden in einer Symboltabelle zusammengefasst. Jeder einzelne Block der Tabelle enthält also eine Symboltabelle mit denjenigen Attributwerten, die mehrfach im Block vorkommen. In den Rows werden diese Werte durch Zeiger auf den entsprechenden Wert in der Symboltabelle ersetzt (siehe Grafik 1). SQL> select firstname, lastname, street, zipcode, city from address; FIRSTNAME LASTNAME STREET ZIPCODE CITY ------------------------------------------------------------- Peter Otto Franz Xaver Michael Josef Huber Huber Huber Meier Meier Meier Freischuetz-Str. Marsstrasse Jupiterstrasse. Leopoldstrasse Marienplatz Goethe-Platz. uncompressed Munich Hamburg Munich Hamburg Munich Frankfurt compressed Blockheader Blockheader Free Space 81927 22085 81927 22085 81927 60322 Direct Path Insert Josef | Meier | Goethe-Platz | 60322 | Frankfurt Michael | Meier | Marienplatz | 81927 | Munich | Josef | * | Goethe-Platz | 5555 | Frankfurt | Michael Xaver | Meier | Leopoldstrasse | 22085 | Hamburg | | * | Marienplatz | * | * | Xaver | * | Leopoldstrasse Franz | Huber | Jupiterstrasse | 81927 | Munich | | * | * | Franz | * | Jupiterstrasse | * | * | Otto | * | M Otto | Huber | Marsstrasse | 22085 | Hamburg | arsstrasse | * | * | Peter | * | Freischuetz-Str. | * | * | Peter | Huber | Freischuetz-Str. | 81927 | Munich | Huber | Meier | Munich | Hamburg | 81927 | 22085 Symboltable Grafik 1 Konzept / Architektur Betrachten wir das Beispiel aus Grafik 1 und nehmen wir an, in unserer Adresstabelle befinden sich 100.000 Einträge mit dem Nachnamen „Meier“. In einer konventionellen, unkomprimierten Tabelle muss die Zeichenkette „Meier“ entsprechend 100.000-mal gespeichert werden. Komprimieren wir die Tabelle, dann wird die Zeichenkette nur noch ein einziges Mal pro Block, nämlich in der Symboltabelle, gespeichert. Jetzt könnte es natürlich sein, dass der Name „Meier“ gerade im Raum München sehr häufig vorkommt. Womöglich befinden sich mehrere Adressen mit Namen „Meier“ und Wohnort „München“ in einem Datenblock. In diesem Fall wird in den betroffenen Datenblöcken, die Symboltabelle um den String „München“ erweitert und auch dieser Wert muss nur noch einmal pro Block gespeichert werden. Bereits hier zeigt sich, dass die Effektivität der Komprimierung sehr stark von der Datenverteilung abhängt. Zur Unterstützung der Komprimierung mussten im Oracle Kern nur einige interne Routinen zum Formatieren von Blöcken sowie Schreiben und Auslesen von Tabellenattributen, modifiziert werden. Für alle anderen Datenbankfunktionen, insbesondere die SQL-Verarbeitung, sind komprimierte Blöcke vollkommen transparent. Für die (UNIX) „Hacker“ unter den Lesern: Das Konzept lässt sich relativ einfach mittels folgenden kleinen Testablaufes verifizieren: connect system/manager REM 1. Erzeugen der Tablespaces create tablespace ts_uncomp datafile '/tmp/ts_uncomp.dbf' size 5M; create tablespace ts_comp datafile '/tmp/ts_comp.dbf' size 5M; REM 2. standard (unkomprimiert) Tabelle erzeugen create table tab_uncomp (i number, x char(100)) pctfree 0 tablespace ts_uncomp; REM 3. Testdaten generieren begin FOR i IN 1..1000 LOOP insert into tab_uncomp values (i,'DAUNENJACKE'); END LOOP; commit; end; / REM 4. komprimiertes Pendant der Testtabelle erzeugen create table tab_comp compress tablespace ts_comp as select * from tab_uncomp; REM 5. wie viele Blöcke umfassen die Tabellen select dbms_rowid.rowid_block_number(rowid) rid, count(*) blocks_uncomp from tab_uncomp group by rollup (dbms_rowid.rowid_block_number(rowid)); select dbms_rowid.rowid_block_number(rowid) rid, count(*) blocks_comp from tab_comp group by rollup (dbms_rowid.rowid_block_number(rowid)); REM 6. wie viele Rows enthält ein Block im Durchschnitt select round(avg(count(*)),0) avg_rows_uncomp from tab_uncomp group by dbms_rowid.rowid_block_number(rowid); select round(avg(count(*)),0) avg_rows_comp from tab_comp group by dbms_rowid.rowid_block_number(rowid); alter system checkpoint; REM 7. Anzahl des String "DAUNENJACKE" in den Dateien host strings /tmp/ts_uncomp.dbf | grep DAUNENJACKE | wc -l host strings /tmp/ts_comp.dbf | grep DAUNENJACKE | wc -l drop tablespace ts_uncomp including contents and datafiles; drop tablespace ts_comp including contents and datafiles; Listing 1 Übrigens: Eine Tabelle, die nur einen Oracle Block benutzt, kann nicht komprimiert werden ;-). Mit der EMP Tabelle funktioniert das Ganze also nicht, obwohl die Tabelle laut DBA/USER_TABLES komprimiert erscheint (COMPRESSION = ENABLED). Die Spalte „COMPRESSION“ existiert erst ab Version 9.2.0.3 (Bug# 2474106). Dieses Beispiel ist natürlich bewusst sehr einfach gehalten. Der Trick mit dem Strings Kommando funktioniert auch nur mit CHAR Feldern. Manche werden sich jetzt fragen: Wie sieht das Ganze mit NUMBER und DATE Attributen aus? Klar, dann geht es nicht ganz so einfach, ein wenig Kreativität und einem Hexeditor oder Erfahrung mit Oracle Blockdumps vorausgesetzt, ist auch das nicht all zu kompliziert. Welche Objekt- und Datentypen sind komprimierbar? - Tabellen - Materialized Views - Einzelne Tabellenpartitionen Es besteht die Möglichkeit, die Komprimierung auf Tablespace-, Table- oder Partition-Ebene zu aktivieren. Die Aktivierung auf Tablespace-Ebene spielt dabei für die betriebliche Praxis vermutlich eine untergeordnete Rolle. Mit Ausnahme aller Arten des Datentypes LOB, sowie allen darauf basierenden Datentypen, wie z.B. XMLTYPE, VARRAY, sind alle Oracle Datentypen unterstützt. Redundante Attributwerte im Block werden mittels Stringvergleich erkannt. CHAR Attribute werden immer in der definierten Länge abgespeichert und bei Bedarf mit Leerzeichen aufgefüllt (blank padded). Im Unterschied zur SQL Semantik sind demzufolge zwei CHAR Attribute mit identischem Wert, aber unterschiedlicher Länge nicht gleich und werden damit auch nicht komprimiert. Die Komprimierung arbeitet Spalten übergreifend. Dies gilt auch dann, wenn es sich um ein CHAR und eine VARCHAR2 Attribut handelt. Entscheidend ist letztendlich der physisch gespeicherte String. Um auch DATE Attribute optimal komprimieren zu können, empfiehlt es sich, gegebenenfalls auf Minuten- bzw. Sekundengenauigkeit zu verzichten. Wie komprimieren wir unsere Tabellen? Die Komprimierung wird ausschließlich in Verbindung mit so genannten Direct Path Inserts wirksam. Neue Daten werden dabei immer „hinter“ der High Water Mark eingefügt. Die Komprimierung wirkt nur beim Erzeugen neuer Blöcke. Ein einmal erstellter, komprimierter Datenblock ist in sich abgeschlossen und steht für weitere Inserts auch nach Delete Operationen nicht mehr zur Verfügung. Komprimierte Segmente werden implizit mit PCTFREE=0 erstellt. PCTUSED besitzt keine Relevanz, es wird keine Freiliste verwaltet. Folgende Direct Path Inserts sind verfügbar: - create table … compress … as select (CTAS) - serial insert mit append Hint ( insert /*+ append */ into … select from) - parallel insert - alter table … move compress - Direct Path SQL*Loader Wie verhalten sich DML-Operationen? Standard DML Operationen erzeugen nach wie vor nicht komprimierte Datenblöcke! • Delete Durch Delete frei gewordener Speicherplatz in komprimierten Blöcken wird nicht wieder verwendet. Dadurch entsteht in der Folge eine Speicherfragmentierung. Im schlechtesten Fall (historische Tabelle, es werden laufend veraltete Einträge gelöscht und neue eingefügt), kann der Speicherverbrauch einer komprimierten Tabelle dabei sogar den eines unkomprimierten Pendants übersteigen. Die Komprimierung wirkt dann kontraproduktiv. Delete Operationen auf • • komprimierte Tabellen benötigen weniger Redo Speicher. Laut Oracle sind sie dadurch um ca. 10% schneller [Quelle: Oracle White Paper]. Update Rows in einem komprimierten Segment können physisch nicht im gleichen Block modifiziert werden. Aus diesem Grund führt jeder Update zwangsläufig zu einer Chained Row. Eine Referenz im Originalblock verweist auf den neuen Block der Row. Der neue Block wird unkomprimiert erstellt und dient damit auch als potenzieller „Behälter“ für Standard Inserts und weitere Chained Rows. Außerdem können etwaige weitere Updates auf die gleiche Row in diesem Block erfolgen. Laut Oracle sind Update Operationen auf komprimierte Segmente zwischen 10-20% langsamer [Quelle: Oracle White Paper]. Inserts (Standard, nicht Direct Path Insert) Standard Insert Operationen bewirken keine Komprimierung. Sie erzeugen beziehungsweise erweitern ausschließlich nicht komprimierte Datenblöcke. Dabei ergeben sich keinerlei Auswirkungen auf die Performance. Ein verhältnismäßig geringer Anteil „normaler“ Inserts kann damit durchaus, ohne negative Nebenwirkungen, auf komprimierte Tabellen erfolgen. Kompressionsrate Der Quotient aus der Anzahl Blöcke einer unkomprimierten Tabelle und dem komprimierten Pendant der Tabelle beschreibt die jeweilige Effektivität der Komprimierung: Kompressio nsrate = # Blocks unkomprimi erte Tabelle # Blocks komprimierte Tabelle Die Kompressionsrate wird von folgenden Faktoren maßgeblich beeinflusst: 1. Kardinalität Entscheidend ist hier die Anzahl der Tabellenspalten, die einen hohen Anteil Duplikate aufweisen, sowie deren Verteilung auf die Böcke. Grundsätzlich gilt: Je mehr Duplikate ein Datenbankblock enthält, umso effektiver wirkt die Komprimierung. 2. Sortierung Eine geeignete Sortierung kann die Kompressionsrate günstig beeinflussen. Bestimmte Attributwerte werden dabei gezielt auf Blockranges verteilt und können damit optimal komprimiert werden. 3. Blockgröße Da jeder Block eigenständig (self contained) ist, also seine eigene Symboltabelle enthält, reduziert sich der für die Komprimierung notwendige Overhead mit zunehmender Blockgröße. Mit der damit auch verbundenen höheren Recorddichte pro Block steigt außerdem die Wahrscheinlichkeit zusätzlicher Redundanzen. Insgesamt ist der Einfluss der Blockgröße im Vergleich zu Punkt 1 (Datenverteilung) und 2 (Sortierung) jedoch als gering anzusehen. In Verbindung mit den genannten Faktoren, sowie in Abhängigkeit von der durchschnittlichen Satzlänge, spielt natürlich auch die Feldlänge eine Rolle. Je kleiner die Satzlänge ist, umso höher ist die mögliche Anzahl Rows pro Block. Mit einer höheren Anzahl Rows pro Block gewinnt die Effektivität der Komprimierung relativ kleiner Attribute. Umgekehrt verhält es sich mit langen Attributen bei größeren Satzlängen. Gleichzeitig besteht aber auch eine Wechselwirkung mit der Kardinalität und Sortierung. Bei genauer Betrachtung erweist sich eine aussagekräftige Schätzung der Komprimierbarkeit von Segmenten als relativ kompliziert !. Einen Einflussfaktor isoliert zu betrachten, reicht in den allermeisten Fällen nicht aus, um brauchbare Rückschlüsse auf mögliche Kompressionsraten abzuleiten. Welche Tabellen komprimieren? Es stellt sich die Frage: Wie können wir geeignete Tabellen identifizieren und was gilt es dabei zu berücksichtigen? Primär interessieren uns zunächst folgende Tabelleneigenschaften: 1. Segmentgröße Diese lässt sich einfach mittels der bekannten Dictionary Views ermitteln. Große Segmente verfügen hier naturgemäß über das beste Potenzial. Auf der anderen Seite können auch kleine Segmente, die aber sehr häufig (loop) oder von vielen Benutzer per Full Table Scan gelesen werden, in der Summe durchaus relevante physical I/O Raten verursachen. 2. Komprimierbarkeit (potenzielle Kompressionsrate) Die Frage nach der Komprimierbarkeit gestaltet sich schon etwas schwieriger. Folgende Befehlssequenz hilft uns, die mögliche Kompressionsrate einer Tabelle abzuschätzen: set serveroutput on declare pct number; -- sample percent blkcnt number; -- block count uncompressed blkcntc number; -- block count compressed begin execute immediate 'create table TEMP$$FOR_TEST pctfree 0 as select * from <TABLE_NAME> where rownum < 1'; pct := 0.00099; blkcnt := 0; while ((pct < 100) and (blkcnt < 1000)) loop execute immediate 'truncate table TEMP$$FOR_TEST'; execute immediate 'insert /*+ APPEND */ into TEMP$$FOR_TEST (select * from <TABLE_NAME> sample block ('||pct||'))'; commit; execute immediate 'select count(distinct(dbms_rowid.rowid_block_number(rowid))) from TEMP$$FOR_TEST' into blkcnt; pct := pct * 10; end loop; execute immediate 'alter table TEMP$$FOR_TEST move compress '; execute immediate 'select count(distinct(dbms_rowid.rowid_block_number(rowid))) from TEMP$$FOR_TEST' into blkcntc; execute immediate 'drop table TEMP$$FOR_TEST'; dbms_output.put_line(round(blkcnt/blkcntc,2)); end; Listing 2 Die jeweilige Kompressionsrate wird dabei mittels Blocksampling, einer Art zufälliger Stichprobe ermittelt. Es handelt sich insofern um eine Schätzung. Bei kleineren bis mittleren Tabellen mit einer hohen Anzahl leerer Datenblöcke oder Tabellen mit sehr langen VARCHAR2 Attributen (>1000 Zeichen), kann die Schätzung zu ungenau ausfallen. Abhilfe schafft in solchen Fällen eine größere Stichprobe („blkcnt“ in der Schleife erhöhen). Eine relativ schnelle und einfache Analyse eines kompletten Oracle Schemas, mit mehreren 100 oder sogar 1000 Tabellen, ist damit allerdings nach wie vor relativ umständlich zu bewerkstelligen. Aus diesem Grund haben wir ein Script erstellt, das eine Schätzung der potenziellen Kompressionsrate einzelner oder auch aller Tabellen eines Schemas ermittelt. In einem realen Kundenprojekt haben wir damit ein Data Warehouse Schema mit mehr als 3000 Tabellen analysiert. Berücksichtigt wurden in diesem Zusammenhang nur Tabellen größer 100 MB. Der folgende kurze Ausschnitt der Ergebnisliste zeigt nur die 10 Tabellen mit dem jeweils besten Einsparungspotenzial (MB_SAVE). Mittels konventioneller Reorganisation der Tabelle und PCTFREE=0, kann die in Spalte CR_R aufgeführte Kompressionsrate erzielt werden. Die Schätzung der möglichen Kompressionsrate durch eine Datensegment Komprimierung sehen wir in der Spalte CR_C. Wie das folgende Beispiel zeigt, sind Kompressionsraten zwischen 2 und 5 in einer realen Umgebung dabei durchaus realistisch. TABELLE ---------TAB1_XXX TAB9_YYY TAB3_ZZZ TAB5_XXX TAB4_AAA TAB2_XXX TAB7_YYY TAB6_XXX TAB1_AAA TAB9_BBB PAR MB_USED MB_REORG MB_COMP CR_R CR_C MB_SAVE --- ---------- ---------- ---------- ------ ------ ---------YES 55835.76 47703.55 11158.45 1.17 4.28 44677.31 NO 34288.03 30965.88 6013.25 1.11 5.15 28274.78 YES 10680.27 10029.98 3125.00 1.06 3.21 7555.27 NO 10412.53 9438.12 3519.57 1.10 2.68 6892.96 YES 7224.44 4898.99 1794.51 1.47 2.73 5429.93 NO 6217.16 4898.99 2062.02 1.27 2.38 4155.14 YES 6131.33 4898.99 2067.55 1.25 2.37 4063.78 YES 4870.05 4316.60 1632.73 1.13 2.64 3237.32 YES 5277.07 4649.62 2201.70 1.13 2.11 3075.37 NO 4007.47 3546.40 1156.88 1.13 3.07 2850.59 … Listing 3 Die Ausgabe kann natürlich auch als Textdatei, in der die einzelnen Werte durch Komma getrennt sind (csv), erfolgen und eignet sich damit als Input für eine Spreadsheet Application. Space Utilisation "TESTSchema" Unused Space GB 2.00 1% Save Compression GB 122.00 51% Compressed Table GB 78.00 33% Save Reorg GB 35.00 15% Grafik 2 Beispiel Analyseergebnis Grafik 2 visualisiert die kumulierten Summen der Analyse: Unused Space Save Compression Save Reorg = Differenz von tatsächlich benutztem Blöcken zu reservierten Blöcken = geschätzte Einsparung durch Table Compression = geschätzte Einsparung durch Reorganisation und Aufbau der Segmente mit PCTFREE=0 Compressed Table = verbleibender netto Memorybedarf Das Script „ldtabcom.sql“ steht zum kostenfreien Download auf der Trivadis Webseite zur Verfügung: http://www.trivadis.com/de/Tools und Services/Downloads Das Script liefert uns die Grundlage, um im nächsten Schritt das DML-Profil potenziell interessanter Tabellen zu analysieren. 3. Zugriffsprofil – DML Spätestens an dieser Stelle enden die Automatismen. Jetzt sind fundierte Kenntnisse der jeweiligen DML Operationen, insbesondere im Rahmen der ETL Prozesse, gefragt. Statische Tabellen sind natürlich für die Komprimierung ideal. Leider findet man in der Praxis große statische Tabellen vergleichsweise selten. Von Oracle als Data Warehouse Feature positioniert, adressiert die Datensegment Komprimierung in erster Linie große Fakt-Tabellen. Diese sind häufig partitioniert und wachsen kontinuierlich. Updates spielen eine untergeordnete Rolle. Inserts erfolgen oftmals ohnehin bereits als Direct Path Inserts oder können einfach umgestellt werden. Im Falle chronologisch partitionierter Tabellen besteht auch die Möglichkeit, die jeweils aktuelle Partition unkomprimiert zu belassen und nur die historischen Partitionen zu komprimieren. Auf diese Weise lassen sich die Vorteile beider Speicherungsformen sehr gut kombinieren. Was gilt es sonst noch zu beachten? An dieser Stelle möchte ich nur noch auf einen, aber dafür sehr wichtigen Punkt hinweisen. Durch die Komprimierung ändern sich einige für die Kosten basierte Optimierung elementare Parameter, wie beispielsweise die Segmentgröße oder der Clustering Faktor von Indizes. Als Folge können sich natürlich auch die jeweiligen Ausführungspläne ändern. Ja klar, werden sie sich jetzt denken, genau das wollen wir ja auch. Grundsätzlich stimmt das natürlich. In der Praxis hat sich jedoch gezeigt, dass an dieser Stelle das „Gute“ von dem „Bösen“ nicht all zu weit entfernt liegt. Tendenziell führt die Komprimierung und die damit verbundene Reduzierung der CBO Kosten zu einer höheren Anzahl Full Table Scans. Umgekehrt kann eine Tabelle die vor der Komprimierung per Full Table Scan gelesen wurde, aufgrund eines besseren Clustering Faktors, anschließend einen Index Range Scan auslösen. Diese Konsequenzen sollte man auf jeden Fall im Hinterkopf behalten. Fazit • Mit Ausnahme von Read Only Tables und eventuell chronologisch partitionierter Tabellen, ist die Komprimierung für OLTP-Umgebungen weniger geeignet. • DWH-Umgebungen können von der Tabellenkomprimierung sehr gut profitieren. • Die Komprimierung erfolgt transparent, es sind keinerlei Modifikationen an den Anwendungen erforderlich. • Die Komprimierung erfordert aktives Management der Objekte. o Die Auswahl geeigneter Tabellen erfordert fundierte Kenntnisse der jeweiligen DML Verarbeitung und ETL Prozesse. Gegebenenfalls müssen diese auf Direct Path Inserts umgestellt werden. o Mit Ausnahme von statischen Tabellen ist meistens eine regelmäßige Reorganisation erforderlich. Dazu müssen die betrieblichen Rahmenbedingungen, wie beispielsweise Wartungsfenster, Prozeduren und Scripte geschaffen werden. • Das Konzept und die damit verbundenen Implikationen müssen beim Einsatz berücksichtigt werden. Verwendete Dokumentation Oracle9i Performance Tuning Guide and Reference Part No. A96533-02 http://otn.oracle.com/products/bi/pdf/o9ir2_compression_twp.pdf Oracle Metalink Note: 228082.1 Sollte ihnen dieser komprimierte Überblick nicht ausreichen, oder wenn sie weitere Fragen zu Risiken und Nebenwirkungen haben, dann besuchen sie doch einfach einen unserer Performance steigernden Kurse oder wenden sich an den Consultant ihres Vertrauens. Jederzeit unkomprimierten Spass mit komprimierten Tabellen wünscht Ihnen Trivadis GmbH Peter Hulm Freischützstraße 92 D-81927 München Internet: http://www.trivadis.com Mail: [email protected] Tel: Fax: +49-89-992 759 30 +49-89-992 759 59