Ein Toast auf PostgreSQL

TOAST - The Oversized-Attribute Storage Technique

Vor PostgreSQL 7.1 war die Länge einer Tabellenzeile auf die Größe einer Speicherseite (Blocksize, BLCKSZ) beschränkt. Ein Datensatz konnte daher nicht länger als 8 bis maximal 32 KB sein. Grössere Datenmengen mussten als Large Objects gespeichert werden und konnten nicht einfach per INSERT in eine Tabelle eingefügt werden. Das bedeutet, dass extra Funktionen zum Speichern oder Auslesen der Large Objects aufgerufen werden müssen, oder, dass aus Applikationen heraus nur innerhalb einer Transaktion auf Large Objects zugegriffen werden kann. Ausserdem werden Large Objects bei einem Dump nicht automatisch exportiert.

Mit der Version 7.1 kam TOAST, ein Verfahren, mit dem Tabellenzeilen bis zu einer Größe von 1 GB (inklusive Overhead) gespeichert werden können. (Dies ist ein theoretischer Wert, der in der Praxis nur selten ereicht wird.)

Das hat den Vorteil, dass sehr grosse Datenmengen in eine Tabelle geschrieben werden können, ohne die besonderen Import- und Exportfunktionen für Large objects benutzen zu müssen. In einer Applikation bedarf es keiner Transaktion, um auf die Daten zuzugreifen und bei einem Dump werden sie automatisch exportiert, wie alle anderen Spaltenwerte auch. Allerdings erlauben getoastete Daten im Gegensatz zu Large Objects keinen wahlfreien Zugriff. Sie werden immer als ganze Objekte gelesen oder geschrieben, wogegen bei Large Objects Teile beschrieben oder ausgelesen werden können.

Wie funktioniert TOAST?

TOAST ist eine Kombination aus Datenkompression und Auslagerung der Daten in eine zusätzliche Hilfstabelle. Jeder Tabelle, die mit CREATE TABLE erzeugt wurde, ist eine primäre Heap-Datei auf der Platte zugeordnet. Enthält diese Tabelle Felder variabler Länge, wie etwa varchar, text oder array, wird automatisch eine Hilfstabelle (TOAST-Tabelle) angelegt. Sobald eine Zeile größer als ein Viertel einer Speicherseite ist, tritt TOAST in Aktion:

TOAST komprimiert das größte Feld mit einem variablen Datentyp (varlen-Attribut). Ist die Zeile immer noch größer als BLKSZ/4, wird wiederum das längste varlen-Attribut komprimiert. Ist die Zeile immer noch zu groß, wird das größte varlen-Attribut in die TOAST-Tabelle ausgelagert. Das geht solange, bis die Größe des Datensatzes unter BLKSZ/4 ist.

Wenn es erforderlich ist, werden die ausgelagerten Daten nochmals aufgeteilt, und in mehreren Datensätzen in der TOAST-Tabelle abgelegt. Daß Daten in einer bestimmten Spalte getoastet sind, ist für den Benutzer unsichtbar.

Der Name der TOAST-Tabelle enthält den OID (Object Identifier) der Haupttabelle, der sie zugeordnet ist. So bezeichnet pg_toast_xxxxx die TOAST-Tabelle zur Haupttabelle mit dem OID xxxxx. Für die TOAST-Tabelle wird ein Index angelegt, der das Auffinden der getoasteten Daten ermöglicht.

Ein Beispiel

Als Beispiel eine einfache Tabelle 'texte' mit zwei Spalten: contents vom Typ text, ein Datentyp variabler Länge, nr ein integer-Feld für die ID des Textes.

test=# CREATE TABLE texte (contents text, nr int4);
CREATE TABLE

Mit diesem Statement wird ein OID (Object-Identifier) für die Tabelle texte erzeugt und im Systemkatalog pg_class eingetragen. Abfragen kann man diesen OID mit:

test=# SELECT relfilenode FROM pg_class WHERE relname = 'texte';
 relfilenode
-------------
       46272
(1 row)

Ausserdem legt PostgreSQL eine TOAST-Tabelle und einen Index an, deren OIDs ebenfalls im Systekatalog pg_class abgelegt werden. Die blosse Existenz der TOAST-Tabelle und des zugehörigen Index kostet keinen zusätzlichen Overhead. Die folgende Abfrage gibt die Referenzen dieser Objekte zurück und in der Spalte relpages, wieviele Speicherseiten den Objekten zugeordnet sind.

test=# SELECT relname, relfilenode, relpages FROM pg_class
test-# WHERE relname = 'pg_toast_46272' OR
test-#       relname = 'pg_toast_46272_index';
       relname        | relfilenode | relpages
----------------------+-------------+----------
 pg_toast_46272_index |       46276 |        1
 pg_toast_46272       |       46274 |        0
(2 rows)

Hier existiert eine Referenz auf die TOAST-Tabelle die im Dateisystem zwar existiert aber 0 Byte gross ist und 0 Speicherseiten belegt. Der Toaster wird nur dann aktiviert, wenn die Länge einer Tabellenzeile mehr als ein Viertel einer Speicherseite belegen würde. Bei 8 KB-Speicherseiten sind das ein paar Byte unter 2KB. Die Daten in den einzelnen Spalten der Tabelle 'texte' sind demnach unter dieser Grenze, so dass nicht ausgelagert werden muss.

Nachdem die Tabelle 'texte' mit einigen grossen Texten gefüllt wurde, sehen diese Abfrageergebnisse wie folgt aus, allerdings erst, nachdem ein VACUUM durchgeführt wurde.

test=# SELECT relfilenode, relpages FROM pg_class
test-# WHERE relname = 'texte';
 relfilenode | relpages
-------------+----------
       46272 |        4
(1 row)

test=# SELECT relname, relfilenode, relpages FROM pg_class
test-# WHERE relname = 'pg_toast_46272' OR
test-#       relname = 'pg_toast_46272_index';
       relname        | relfilenode | relpages
----------------------+-------------+----------
 pg_toast_46272_index |       46276 |        2
 pg_toast_46272       |       46274 |        3
(2 rows)

Die Vorteile von TOAST

Die Vorteile von TOAST liegen auf der Hand: Wenn Tabellen grosse Datenmengen enthalten, werden sie niemals so gross wie ohne TOAST, da nur ein Teil der Daten in der Haupttabelle gespeichert sind. Bei Abfragen, in denen das getoastete Feld nicht in der Feldliste steht, wird nicht der gesamte Inhalt in den Speicher geladen. Beispielsweise wenn in unserer einfachen Tabelle das Feld nur upgedatet wird.

"Unter der Annahme dass normalerweise kleine Felder als Schlüsslewerte benutzt werden, bei Anfragen, die z.B. unsere einfache Tabelle mit 2-3 anderen Tabellen in einem Join verknüpfen und diverse Sortier- und Mischstufen benötigen, wandern nur die kleinen Haupttabellen Zeilen mit ihren Toast Referenzen duch die diversen Stufen. Wenn das endgültig gefilterte Ergebnis zur Anwendung gesandt wird werden die Toast Werte dazugelesen. Aber diese "Dicken Kinder von Landau" trampeln nicht in den Hashjoin Tabellen herum oder essen viel I/O Kapazizät beim Sortieren." Jan Wieck

Ein weiterer Vorteil von TOAST zeigt sich darin, wie der Speichermanager getoastete Datensätze behandelt. Wenn ein Datensatz in einer Tabelle aktualisiert wird, legt der Speichermanger von PostgreSQL im Normalfall einen neuen Datensatz mit den aktualisierten Daten an. Die Originalzeile wird nicht überschrieben, sondern als veraltet markiert. Erst ein VACUUM löscht diesen veralteten Datensatz. Ohne TOAST müsste der aktualisierte Datensatz eine Kopie aller Daten enthalten und ein VACUUM haette sehr viel Arbeit, die Tabelle wieder zu reorganisieren.

Mit TOAST wird der zuvor getoastete Wert nicht als veraltet markiert, so dass der neue Datensatz ihn übernehmen kann. Er erhält lediglich eine neue Referenz auf die getoasteten Daten. Damit werden viele Schreib- und Lesezugriffe auf die Platte vermieden wodurch VACUUM erheblich beschleunigt wird. Ausserdem liest ein UPDATE niemals getoastete Daten, so dass diese Operation auf einer verkleinerten Tabelle arbeiten kann, was der Geschwindigkeit zugute kommt.

Und wenn ich die Daten nicht toasten möchte ...

Will man vermeiden, dass das System TOAST-Tabellen und -Index anlegt, so muss man Felder so definieren, dass der Datenbankserver selber entscheiden kann, dass eine Tabellenzeile niemals die Größe einer Speicherseite erreichen kann. Enthält die Tabelle nur Integerwerte oder varchar(n), wird keine TAOST-Tabelle angelegt, falls n klein genug ist.

Mit ALTER TABLE ... SET STORAGE kann man nach der Definition einer Tabelle beeinflussen, wie die Werte einer Spalte gespeichert werden sollen. Es kann bestimmt werden, ob die Spaltenwerte in der Tabelle selbst gespeichert werden sollen, ob sie ausgelagert oder komprimiert werden sollen.

ALTER TABLE [ ONLY ] table [ * ]
  ALTER [ COLUMN ] column
  SET STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN }

PLAIN: Die Werte werden unkomprimiert (inline) in der Tabelle gespeichert. Für Werte fester Länge wie INTEGER muss PLAIN benutzt werden.

EXTERNAL: Der Toaster kann die Spaltenwerte unkomprimiert aus der Haupttabelle in die TOAST-Tabelle schreiben. Dann enthält die Haupttabelle nur eine Referenz, die die Daten eindeutig identifiziert.

EXTENDED: Der Toaster kann die Spaltenwerte komprimieren und/oder aus der Haupttabelle in die TOAST-Tabelle schreiben.

"Dies ist der Standard da es der Standard aller vordefinierten Datentypen mit variabler Länge ist. Bei CREATE TABLE wird jede Spalte so konfiguriert wie der gewählte Datentyp konfiguriert ist." Jan Wieck

MAIN: Die Werte werden in der Tabelle komprimiert gespeichert, falls der Datentyp Kompression unterstützt.

"Den Versuch zu komprimieren zu verhindern ist dann sinnvoll wenn die zu speichernden Daten bereits komprimiert sind, wie z.B. B64-kodierte JPEG Bilder. Toast wird niemals in der Lage sein, solche Daten zu komprimieren." Jan Wieck

Ein herzliches Dankeschön an Jan Wieck für seine nette Unterstützung.

©cornelia boenigk