MVCC-5. In-Page-Reinigung und HEISS

Ich möchte Sie daran erinnern, dass wir Probleme im Zusammenhang mit der Isolation untersucht , einen Exkurs über das Organisieren von Daten auf niedriger Ebene durchgeführt und dann ausführlich über Zeilenversionen und das Erhalten von Snapshots aus Versionen gesprochen haben.

Heute werden wir uns mit zwei ziemlich eng verwandten Themen befassen: Bereinigung innerhalb der Seite und HOT-Updates . Beide Mechanismen können als Optimierungen klassifiziert werden. Sie sind wichtig, werden jedoch in der Benutzerdokumentation fast nicht behandelt.

In-Page-Reinigung mit regelmäßigen Updates


Beim Zugriff auf eine Seite - sowohl während des Aktualisierens als auch beim Lesen - kann eine schnelle Bereinigung innerhalb der Seite erfolgen, wenn PostgreSQL versteht, dass auf der Seite nicht genügend Speicherplatz vorhanden ist. Dies tritt in zwei Fällen auf.

  1. Bei einem zuvor auf dieser Seite durchgeführten Update (UPDATE) wurde nicht genügend Speicherplatz gefunden, um eine neue Version der Zeile auf derselben Seite zu platzieren. Diese Situation wird im Seitentitel gespeichert und beim nächsten Löschen der Seite.
  2. Die Seite ist mehr gefüllt als auf fillfactor. In diesem Fall erfolgt die Reinigung sofort, ohne das nächste Mal zu verzögern.

Fillfactor ist ein Speicherparameter, der für die Tabelle (und für den Index) definiert werden kann. PostgreSQL fügt nur dann eine neue Zeile (INSERT) auf der Seite ein, wenn diese Seite weniger als voll oder voll ist. Der verbleibende Speicherplatz ist für neue Versionen von Zeichenfolgen reserviert, die aus Aktualisierungen (UPDATE) resultieren. Der Standardwert für Tabellen ist 100, dh der Speicherplatz ist nicht reserviert (und der Wert für Indizes ist 90).

Die Intra-Page-Bereinigung entfernt Versionen von Zeilen, die in keinem Bild sichtbar sind (außerhalb des "Ereignishorizonts" der Datenbank, über den wir das letzte Mal gesprochen haben ), funktioniert jedoch ausschließlich innerhalb derselben Tabellenseite. Zeiger auf bereinigte Versionen von Zeichenfolgen werden nicht freigegeben, da auf sie aus Indizes verwiesen werden kann und der Index eine andere Seite ist. Die Bereinigung von Seiten geht nie über eine Tabellenseite hinaus, ist jedoch sehr schnell.

Aus den gleichen Gründen wird die Freiraumkarte nicht aktualisiert. Es spart auch Platz für Updates, nicht für Einfügungen. Die Sichtbarkeitskarte wird ebenfalls nicht aktualisiert.

Die Tatsache, dass eine Seite beim Lesen gelöscht werden kann, bedeutet, dass eine Leseanforderung (SELECT) dazu führen kann, dass sich Seiten ändern. Dies ist ein weiterer solcher Fall zusätzlich zu der zuvor verzögerten Änderung von Hinweisbits.

Lassen Sie uns anhand eines Beispiels sehen, wie dies funktioniert. Erstellen Sie eine Tabelle und Indizes für beide Spalten.

=> CREATE TABLE hot(id integer, s char(2000)) WITH (fillfactor = 75); => CREATE INDEX hot_id ON hot(id); => CREATE INDEX hot_s ON hot(s); 

Wenn nur lateinische Buchstaben in Spalte s gespeichert sind, belegt jede Version der Zeile 2004 Bytes plus 24 Bytes des Headers. Wir setzen den Speicherparameter für den Füllfaktor auf 75% - es ist genügend Platz für drei Zeilen vorhanden.

Der Einfachheit halber erstellen wir eine bereits bekannte Funktion neu und ergänzen die Ausgabe durch zwei Felder:

 => CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax text, hhu text, hot text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu, CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL; 

Und lassen Sie uns eine Funktion erstellen, um in die Indexseite zu schauen:

 => CREATE FUNCTION index_page(relname text, pageno integer) RETURNS TABLE(itemoffset smallint, ctid tid) AS $$ SELECT itemoffset, ctid FROM bt_page_items(relname,pageno); $$ LANGUAGE SQL; 

Wir werden überprüfen, wie die Bereinigung innerhalb einer Seite funktioniert. Fügen Sie dazu eine Zeile ein und ändern Sie sie mehrmals:

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; 

Es gibt vier Versionen der Zeile auf der Seite:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3979 (c) | 3980 (c) | | | (0,2) (0,2) | normal | 3980 (c) | 3981 (c) | | | (0,3) (0,3) | normal | 3981 (c) | 3982 | | | (0,4) (0,4) | normal | 3982 | 0 (a) | | | (0,4) (4 rows) 

Wie erwartet haben wir gerade die Füllfaktorschwelle überschritten. Dies wird durch die Differenz zwischen der Seitengröße und den oberen Werten angezeigt: Sie überschreitet den Schwellenwert von 75% der Seitengröße, der 6144 Byte beträgt.

 => SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0)); 
  lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row) 

Wenn Sie das nächste Mal auf die Seite zugreifen, sollte eine Bereinigung innerhalb der Seite erfolgen. Überprüfen Sie dies heraus.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | dead | | | | | (0,2) | dead | | | | | (0,3) | dead | | | | | (0,4) | normal | 3982 (c) | 3983 | | | (0,5) (0,5) | normal | 3983 | 0 (a) | | | (0,5) (5 rows) 

Alle irrelevanten Versionen der Zeilen (0,1), (0,2) und (0,3) werden gelöscht. Danach wird dem frei gewordenen Raum eine neue Version der Zeile (0.5) hinzugefügt.

Die nach dem Bereinigen verbleibenden Zeilenversionen werden physisch an die Seite der Adressen der höheren Seite verschoben, sodass der gesamte freie Speicherplatz durch ein fortlaufendes Fragment dargestellt wird. Die Werte der Zeiger ändern sich entsprechend. Dank dessen gibt es keine Probleme mit der Fragmentierung des freien Speicherplatzes auf der Seite.

Zeiger auf gelöschte Versionen von Zeichenfolgen können nicht freigegeben werden, da auf sie von einer Indexseite verwiesen wird. Schauen wir uns die erste Seite des hot_s-Index an (weil die Null mit Metainformationen beschäftigt ist):

 => SELECT * FROM index_page('hot_s',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) 2 | (0,2) 3 | (0,3) 4 | (0,4) 5 | (0,5) (5 rows) 

Wir werden das gleiche Bild in einem anderen Index sehen:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,5) 2 | (0,4) 3 | (0,3) 4 | (0,2) 5 | (0,1) (5 rows) 

Sie können feststellen, dass die Zeiger auf die Tabellenzeilen hier "rückwärts" gehen, aber das spielt keine Rolle, da in allen Versionen der Zeilen der gleiche Wert id = 1 ist. Im vorherigen Index sind die Zeiger jedoch nach s-Werten geordnet, und dies im Wesentlichen.

Mit dem Indexzugriff kann PostgreSQL (0,1), (0,2) oder (0,3) als Zeilenversionskennung erhalten. Dann wird er versuchen, die entsprechende Zeile von der Tabellenseite abzurufen, aber dank des toten Status des Zeigers wird er feststellen, dass eine solche Version nicht mehr existiert, und sie ignorieren. (Wenn PostgreSQL zum ersten Mal feststellt, dass eine Version einer Tabellenzeile fehlt, ändert PostgreSQL auch den Status des Zeigers auf der Indexseite, sodass nicht erneut auf die Tabellenseite zugegriffen wird.)

Es ist wichtig, dass die Bereinigung innerhalb einer Seite nur innerhalb einer Tabellenseite funktioniert und keine Indexseiten gelöscht werden.

HEISSE Updates


Warum ist es schlecht, Links zu allen Versionen einer Zeichenfolge im Index zu behalten?

Erstens müssen Sie bei jeder Zeilenänderung alle für die Tabelle erstellten Indizes aktualisieren: Da eine neue Version angezeigt wurde, müssen Sie Links dazu haben. Und das müssen Sie auf jeden Fall tun, auch wenn sich Felder ändern, die nicht im Index enthalten sind. Offensichtlich ist dies nicht sehr effektiv.

Zweitens sammeln die Indizes Links zu historischen Versionen der Zeichenfolge, die dann zusammen mit den Versionen selbst gelöscht werden müssen (wir werden dies etwas später betrachten).

Darüber hinaus gibt es ein Merkmal der Implementierung des B-Baums in PostgreSQL. Wenn auf der Indexseite nicht genügend Platz zum Einfügen einer neuen Zeile vorhanden ist, wird die Seite in zwei Teile geteilt und alle Daten werden zwischen ihnen neu verteilt. Dies wird als geteilte Seite bezeichnet. Beim Löschen von Zeilen bleiben jedoch zwei Indexseiten nicht mehr zu einer zusammen. Aus diesem Grund wird die Größe des Index möglicherweise nicht verringert, selbst wenn ein wesentlicher Teil der Daten gelöscht wird.

Je mehr Indizes auf dem Tisch erstellt werden, desto größer sind natürlich die Schwierigkeiten, mit denen Sie konfrontiert sind.

Wenn sich jedoch der Wert einer Spalte ändert, die zu keinem Index gehört, macht es keinen Sinn, einen zusätzlichen Datensatz im B-Baum zu erstellen, der denselben Schlüsselwert enthält. So funktioniert die Optimierung, das so genannte HOT-Update - das Heap-Only-Tupel-Update.

Mit diesem Update gibt es nur einen Eintrag auf der Indexseite, der auf die allererste Version der Zeile auf der Tabellenseite verweist. Und bereits auf dieser tabellarischen Seite ist eine Versionskette organisiert:

  • Zeichenfolgen, die geändert und in die Kette aufgenommen werden, sind mit dem Bit Heap Hot Updated gekennzeichnet.
  • Zeilen, auf die im Index nicht verwiesen wird, sind mit dem Bit "Nur Heap-Tupel" gekennzeichnet (dh "nur die tabellarische Version der Zeile").
  • Die regelmäßige Verknüpfung von Zeichenfolgenversionen über das Feld ctid wird unterstützt.

Wenn PostgreSQL beim Scannen des Index die Tabellenseite aufruft und die als Heap Hot Updated gekennzeichnete Version erkennt, muss es nicht angehalten werden und geht weiter entlang der gesamten Update-Kette. Natürlich wird für alle auf diese Weise erhaltenen Versionen von Zeichenfolgen die Sichtbarkeit überprüft, bevor sie an den Client zurückgegeben werden.

Um die Funktionsweise eines HOT-Updates zu überprüfen, löschen Sie einen Index und löschen Sie die Tabelle.

 => DROP INDEX hot_s; => TRUNCATE TABLE hot; 

Wiederholen Sie das Einfügen und aktualisieren Sie die Zeile.

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; 

Folgendes sehen wir auf der Tabellenseite:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 | t | | (0,2) (0,2) | normal | 3987 | 0 (a) | | t | (0,2) (2 rows) 

Auf der Seite gibt es eine Reihe von Änderungen:

  • Das Heap Hot Updated-Flag zeigt an, dass Sie entlang der ctid-Kette gehen müssen.
  • Das Flag Nur Heap-Tupel zeigt an, dass keine Indexverknüpfungen zu dieser Version der Zeile vorhanden sind.

Mit weiteren Änderungen wächst die Kette (innerhalb der Seite):

 => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 (c) | t | | (0,2) (0,2) | normal | 3987 (c) | 3988 (c) | t | t | (0,3) (0,3) | normal | 3988 (c) | 3989 | t | t | (0,4) (0,4) | normal | 3989 | 0 (a) | | t | (0,4) (4 rows) 

Darüber hinaus gibt es im Index einen einzigen Verweis auf den „Kopf“ der Kette:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) (1 row) 

Wir betonen, dass HOT-Updates funktionieren, wenn die aktualisierten Felder in keinem Index enthalten sind. Andernfalls würde in einem Index ein Link direkt zur neuen Version der Zeichenfolge vorhanden sein, was der Idee dieser Optimierung widerspricht.

Die Optimierung funktioniert nur innerhalb der Grenzen einer Seite. Daher erfordert ein zusätzlicher Bypass der Kette keinen Zugriff auf andere Seiten und beeinträchtigt die Leistung nicht.

In-Page-Reinigung mit HOT-Updates


Ein besonderer, aber wichtiger Fall der Intra-Page-Reinigung ist die Reinigung während der HOT-Updates.

Wie beim letzten Mal haben wir den Füllfaktor-Schwellenwert bereits überschritten, sodass das nächste Update zu einer Bereinigung der Seite führen sollte. Aber diesmal auf der Seite ist eine Kette von Updates. Der "Kopf" dieser HOT-Kette sollte immer an Ort und Stelle bleiben, da der Index darauf verweist und der Rest der Zeiger freigegeben werden kann: Es ist bekannt, dass sie nicht von außen referenziert werden.

Um den "Kopf" nicht zu berühren, wird eine doppelte Adressierung verwendet: Der Zeiger, auf den sich der Index bezieht - in diesem Fall (0,1) - erhält den Status "Umleiten" und leitet zur gewünschten Version der Zeichenfolge um.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | normal | 3989 (c) | 3990 | t | t | (0,2) (4 rows) 

Bitte beachten Sie Folgendes:

  • Versionen (0,1), (0,2) und (0,3) wurden gelöscht,
  • Der Kopfzeiger (0,1) blieb erhalten, erhielt jedoch den Umleitungsstatus.
  • Eine neue Version der Zeile wird an Ort und Stelle geschrieben (0.2), da diese Version garantiert keine Links von Indizes enthält und der Zeiger freigegeben (nicht verwendet) wurde.

Führen Sie das Update noch einige Male durch:

 => UPDATE hot SET s = 'F'; => UPDATE hot SET s = 'G'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 (c) | 3991 (c) | t | t | (0,3) (0,3) | normal | 3991 (c) | 3992 | t | t | (0,5) (0,4) | normal | 3989 (c) | 3990 (c) | t | t | (0,2) (0,5) | normal | 3992 | 0 (a) | | t | (0,5) (5 rows) 

Das folgende Update führt erneut zu einer Bereinigung innerhalb der Seite:

 => UPDATE hot SET s = 'H'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 5 | | | | | (0,2) | normal | 3993 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | unused | | | | | (0,5) | normal | 3992 (c) | 3993 | t | t | (0,2) (5 rows) 

Wieder werden einige Versionen gelöscht und der Zeiger auf den "Kopf" wird entsprechend verschoben.

Schlussfolgerung: Bei häufigen Aktualisierungen von Spalten außerhalb der Indizes kann es sinnvoll sein, den Parameter fillfactor zu reduzieren, um auf der Seite Platz für Aktualisierungen zu reservieren. Natürlich müssen wir berücksichtigen, dass je niedriger der Füllfaktor ist, desto mehr nicht zugewiesener Speicherplatz auf der Seite verbleibt und dementsprechend die physische Größe der Tabelle zunimmt.

HEISSER Kettenbruch


Wenn auf der Seite nicht genügend freier Speicherplatz vorhanden ist, um eine neue Version einer Zeile zu veröffentlichen, wird die Kette unterbrochen. Die Version der Zeile, die auf einer anderen Seite veröffentlicht wird, muss einen separaten Link zum Index erstellen.

Um diese Situation zu erreichen, starten wir eine parallele Transaktion und erstellen darin einen Datenschnappschuss.

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot; 
 | count | ------- | 1 | (1 row) 

Ein Schnappschuss löscht nicht die Version der Zeilen auf der Seite. Jetzt führen wir das Update in der ersten Sitzung durch:

 => UPDATE hot SET s = 'I'; => UPDATE hot SET s = 'J'; => UPDATE hot SET s = 'K'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 | t | t | (0,5) (0,5) | normal | 3996 | 0 (a) | | t | (0,5) (5 rows) 

Wenn die Seite das nächste Mal aktualisiert wird, ist nicht genügend Speicherplatz auf der Seite vorhanden, aber die Bereinigung der Seite kann nichts freigeben:

 => UPDATE hot SET s = 'L'; 

 | => COMMIT; --     

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 (c) | t | t | (0,5) (0,5) | normal | 3996 (c) | 3997 | | t | (1,1) (5 rows) 

In Version (0.5) sehen wir einen Link zu (1.1), der zu Seite 1 führt.

 => SELECT * FROM heap_page('hot',1); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+------+-------+-----+-----+-------- (1,1) | normal | 3997 | 0 (a) | | | (1,1) (1 row) 

Jetzt gibt es zwei Zeilen im Index, von denen jede auf den Anfang ihrer HOT-Kette zeigt:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (1,1) 2 | (0,1) (2 rows) 

Leider fehlen in der Dokumentation praktisch Informationen zur Bereinigung von Seiten und zu HOT-Updates, und die Wahrheit muss im Quellcode gesucht werden. Ich empfehle mit README.HOT zu beginnen .

Fortsetzung folgt .

Source: https://habr.com/ru/post/de449704/


All Articles