Home → PostgreSQL Datenbank → PostgreSQL 19 Beta 3 im Praxistest: Bloat mit dem neuen REPACK-Befehl beseitigen
Wenn du PostgreSQL-Datenbanken über längere Zeit betreibst, kennst du das Thema sicher: Durch UPDATE- und DELETE-Operationen entstehen Dead Tuples und Tabellen können mit der Zeit deutlich mehr Speicherplatz belegen, als es die tatsächlich vorhandene Datenmenge vermuten lässt. Ein normales VACUUM schafft hier zwar innerhalb der Relation wieder Platz für neue Daten – die physische Größe der Tabelle auf dem Dateisystem reduziert sich dadurch in der Regel jedoch nicht.
Mit PostgreSQL 19 kommt dafür eine neue Möglichkeit direkt in den Core: REPACK schreibt eine Tabelle vollständig neu und kann dadurch ungenutzten Speicher tatsächlich wieder an das Betriebssystem zurückgeben.
Das ist nicht nur aus Sicht des Datenbankbetriebs interessant. Gerade bei großen oder über Jahre gewachsenen Datenbanken kann das physische Verkleinern stark aufgeblähter Tabellen auch den benötigten Storage reduzieren und damit Ressourcen effizienter nutzbar machen.
Wie groß der Unterschied zwischen VACUUM und dem neuen REPACK in der Praxis sein kann, zeigt das folgende Beispiel mit PostgreSQL 19 Beta 3.
Die Testumgebung verwendet:
PostgreSQL 19beta3
Zunächst wird eine neue Testdatenbank erstellt:
createdb repacktest Anschließend werden mit pgbench 10 Millionen Datensätze erzeugt:
pgbench -i -s 100 repacktest Verbindung:
psql repacktest
Version:
psql (19beta3)
Datensätze prüfen:
SELECT count(*)
FROM pgbench_accounts;
Ergebnis:
count
----------
10000000 Dieses Statment wird im Folgenden noch häufiger ausgeführt:
SELECT
pg_size_pretty(pg_relation_size('pgbench_accounts')) AS table_size,
pg_size_pretty(pg_indexes_size('pgbench_accounts')) AS index_size,
pg_size_pretty(pg_total_relation_size('pgbench_accounts')) AS total_size;
Ergebnis:
table_size | index_size | total_size
------------+------------+------------
1281 MB | 214 MB | 1495 MB
Damit ergibt sich folgende Ausgangslage:
Kennzahl | Wert
------------+---------------
Datensätze | 10.000.000
Tabelle | 1281 MB
Indizes | 214 MB
Gesamt | 1495 MB Nun werden 50 % der Datensätze gelöscht:
DELETE FROM pgbench_accounts
WHERE aid % 2 = 0;
Ergebnis:
DELETE 5000000
Anzahl der verbleibenden Datensätze:
SELECT count(*)
FROM pgbench_accounts;
Ergebnis:
count
---------
5000000
Die logische Datenmenge wurde damit halbiert.
Es wird das Statement von oben ausgeführt.
Ergebnis:
table_size | index_size | total_size
------------+------------+------------
1281 MB | 214 MB | 1495 MB
Obwohl nur noch die Hälfte der Datensätze vorhanden ist, bleibt die physische Größe unverändert.
Das ist der gewünschte Bloat-Zustand.
SELECT
relname,
n_live_tup,
n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'pgbench_accounts';
Ergebnis:
relname | n_live_tup | n_dead_tup
------------------+------------+------------
pgbench_accounts | 4999844 | 5000000
Damit sind ungefähr 5 Millionen Dead Tuples vorhanden.
SELECT pg_relation_filepath('pgbench_accounts'); Ergebnis:
base/16858/16874 Auf Betriebssystemebene:
du -sh $PGDATA/base/16858/16874 Ergebnis:
1.1G /opt/postgres/ASPICON/PGDATA/base/16858/16874 Auch auf Dateisystemebene wurde also noch kein Speicherplatz zurückgegeben.
Nun wird ein normales VACUUM mit ANALYZE ausgeführt:
VACUUM (VERBOSE, ANALYZE) pgbench_accounts; Auszug aus der Ausgabe:
INFO: vacuuming "repacktest.public.pgbench_accounts"
pages:
0 removed,
163935 remain,
7892 scanned (4.81% of total)
tuples:
44859 removed,
4999913 remain,
0 are dead but not yet removable
index scan bypassed:
1499 pages from table (0.91% of total)
have 45991 dead item identifiers
avg read rate: 126.089 MB/s
avg write rate: 25.898 MB/s
WAL usage:
1500 records,
1452 full page images,
6256905 bytes
elapsed: 0.44 s
Anschließend wurde noch ANALYZE ausgeführt:
INFO: analyzing "public.pgbench_accounts"
"pgbench_accounts":
scanned 30000 of 163935 pages,
containing 915067 live rows and 8777 dead rows
5000384 estimated total rows
elapsed: 0.36 s
Es wird das Statement von oben ausgeführt.
Ergebnis:
table_size | index_size | total_size
------------+------------+------------
1281 MB | 214 MB | 1495 MB
Die physische Größe bleibt unverändert.
SELECT
relname,
n_live_tup,
n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'pgbench_accounts';
Ergebnis:
relname | n_live_tup | n_dead_tup
------------------+------------+------------
pgbench_accounts | 5000384 | 47962
5.000.000 auf nur noch etwa 47.962 reduziert. du -sh $PGDATA/base/16858/16874 Ergebnis:
1.1G /opt/postgres/ASPICON/PGDATA/base/16858/16874 Damit bestätigt sich das erwartete Verhalten:
Merkmal | Vor VACUUM | Nach VACUUM
------------------------+---------------+----------------
Live Tuples | ~5 Mio. | ~5 Mio.
Dead Tuples | ~5 Mio. | ~48.000
Tabellengröße | 1281 MB | 1281 MB
Gesamtgröße | 1495 MB | 1495 MB
Datei im Filesystem | 1.1 GB | 1.1 GB
VACUUM macht den freigewordenen Platz innerhalb der Relation wiederverwendbar, gibt ihn aber normalerweise nicht an das Betriebssystem zurück.
Anschließend wird die Tabelle mit dem neuen PostgreSQL-19-Befehl vollständig neu geschrieben:
REPACK (VERBOSE, ANALYZE) pgbench_accounts; Ausgabe:
INFO: repacking "public.pgbench_accounts" in physical order
INFO: "public.pgbench_accounts":
found 0 removable,
5000000 nonremovable row versions
in 163935 pages
DETAIL:
0 dead row versions cannot be removed yet.
CPU:
user: 14.04 s,
system: 17.55 s,
elapsed: 56.52 s
Anschließend:
INFO: analyzing "public.pgbench_accounts"
"pgbench_accounts":
scanned 30000 of 81968 pages,
containing 1829952 live rows
and 0 dead rows
4999917 estimated total rows
Auffällig ist bereits hier die Anzahl der Seiten:
REPACK: 163935 pagesREPACK: 81968 pages
Die Anzahl der Heap-Seiten wurde damit praktisch halbiert.
Es wird das Statement von oben ausgeführt.
Ergebnis:
table_size | index_size | total_size
------------+------------+------------
640 MB | 107 MB | 748 MB
Damit ergibt sich:
Kennzahl | Vor REPACK | Nach REPACK | Veränderung
----------+--------------+---------------+--------------
Tabelle | 1281 MB | 640 MB | ca. -50 %
Indizes | 214 MB | 107 MB | ca. -50 %
Gesamt | 1495 MB | 748 MB | ca. -50 % Das entspricht sehr genau der zuvor halbierten Datenmenge.
SELECT
relname,
n_live_tup,
n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'pgbench_accounts';
Ergebnis:
relname | n_live_tup | n_dead_tup
------------------+------------+------------
pgbench_accounts | 4999917 | 0
Nach dem Rewrite befinden sich damit keine Dead Tuples mehr in der neuen Relation.
Besonders interessant ist die Änderung des Relation-Pfades.
Vor dem REPACK:
base/16858/16874 Nach dem REPACK:
SELECT pg_relation_filepath('pgbench_accounts'); Ergebnis:
base/16858/16884 PostgreSQL verwendet nach dem Rewrite also eine neue physische Relation.
Die alte Datei:
du -sh $PGDATA/base/16858/16874 Ergebnis:
0 /opt/postgres/ASPICON/PGDATA/base/16858/16874 Die neue Datei:
du -sh $PGDATA/base/16858/16884 Ergebnis:
641M /opt/postgres/ASPICON/PGDATA/base/16858/16884 Der Test zeigt sehr deutlich den Unterschied zwischen VACUUM und REPACK.
10.000.000 Rows
Table: 1281 MB
Indexes: 214 MB
Total: 1495 MB
Nach dem Löschen von 50 % der Daten:
5.000.000 Live Tuples
5.000.000 Dead Tuples
Table: 1281 MB
Indexes: 214 MB
Total: 1495 MB
Die physische Größe bleibt unverändert.
~5.000.000 Live Tuples
~48.000 Dead Tuples
Table: 1281 MB
Indexes: 214 MB
Total: 1495 MB
Der Platz ist intern wiederverwendbar, wird aber nicht an das Betriebssystem zurückgegeben.
~5.000.000 Live Tuples
0 Dead Tuples
Table: 640 MB
Indexes: 107 MB
Total: 748 MB
Damit wurden ungefähr 50 % des Speicherplatzes tatsächlich zurückgewonnen.
Zustand | Live Tuples | Dead Tuples | Tabelle | Indizes | Gesamt
---------------+---------------+---------------+-----------+-----------+----------
Initial | 10 Mio. | ~0 | 1281 MB | 214 MB | 1495 MB
nach DELETE | 5 Mio. | 5 Mio. | 1281 MB | 214 MB | 1495 MB
nach VACUUM | ~5 Mio. | ~48 Tsd. | 1281 MB | 214 MB | 1495 MB
nach REPACK | ~5 Mio. | 0 | 640 MB | 107 MB | 748 MB Das praktische Ergebnis lässt sich daher sehr einfach zusammenfassen:
DELETE
↓
Dead Tuples entstehen
↓
VACUUM
↓
Platz innerhalb PostgreSQL wiederverwendbar
↓
Relation bleibt physisch groß
REPACK
↓
kompletter Rewrite
↓
neue Heap-Datei
↓
neue Indexdateien
↓
physischer Platz wird zurückgewonnen
Der nächste Schritt besteht darin, denselben Test unter laufendem Workload durchzuführen.
Beispielsweise:
pgbench -c 20 -j 4 -P 5 -T 300 repacktest Parallel dazu:
REPACK (CONCURRENTLY, VERBOSE, ANALYZE) pgbench_accounts; Während ein normales REPACK die Tabelle durch einen ACCESS EXCLUSIVE Lock blockiert, wird dieser Lock bei CONCURRENTLY im Wesentlichen erst für den abschließenden Austausch benötigt.
Änderungen während des Neuaufbaus werden über Logical Decoding erfasst und auf die neue Relation übertragen.
Der Ablauf lässt sich vereinfacht folgendermaßen darstellen:
laufender Workload
|
INSERT / UPDATE / DELETE
|
v
Originalrelation --------------------------+
| |
| bestehende Daten |
v |
Neue Relation |
^ |
| |
+---------- Logical Decoding -------+
|
v
Änderungen nachziehen
|
v
ACCESS EXCLUSIVE
kurzer Swap
|
v
neue Relation
Damit eignet sich REPACK (CONCURRENTLY) insbesondere für große Tabellen, bei denen ein vollständiger blockierender Rewrite betrieblich problematisch wäre.
Die PostgreSQL-Dokumentation weist ausdrücklich darauf hin, dass REPACK (CONCURRENTLY) nicht vollständig MVCC-safe ist (MVCC = Multi-Version Concurrency Control). Das ist insbesondere für bereits laufende Transaktionen mit älteren Snapshots relevant.
Beispiel:
-- Session 1
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT *
FROM andere_tabelle;
Damit existiert ein Snapshot, der vor dem Repack entstanden ist. Die Tabelle pgbench_accounts wurde innerhalb dieser Transaktion bislang noch nicht verwendet.
Parallel:
-- Session 2
REPACK (CONCURRENTLY) pgbench_accounts;
Nach Abschluss des Rewrite greift Session 1 erstmals auf die Tabelle zu:
SELECT count(*)
FROM pgbench_accounts;
Da die physische Relation zwischenzeitlich ausgetauscht wurde, kann die alte Snapshot-Sicht nicht in jedem Fall korrekt auf die neue Relation übertragen werden. In bestimmten Situationen kann die Tabelle für eine solche Transaktion sogar leer erscheinen.
Besonders beachten sollte man dies bei:
REPEATABLE READ-TransaktionenSERIALIZABLE-Transaktionen
Vor einem REPACK (CONCURRENTLY) sollte daher geprüft werden, ob alte bzw. lang laufende Transaktionen vorhanden sind:
SELECT
pid,
usename,
application_name,
state,
xact_start,
now() - xact_start AS transaction_age
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
Damit ist REPACK (CONCURRENTLY) zwar weitgehend online, aber nicht vollständig transparent gegenüber allen bestehenden MVCC-Snapshots.
Der PostgreSQL-19-Befehl REPACK ist funktional sehr ähnlich zur bekannten Extension pg_repack, stellt aber keine einfache Übernahme der Extension in den PostgreSQL-Core dar. pg_repack bleibt ein eigenständiges Projekt.
Der wesentliche Unterschied liegt insbesondere in der technischen Umsetzung der parallelen Änderungen.
Die folgende Tabelle vergleicht die Eigenschaften von pg_repack und PostgreSQL-19 miteinander:
|
Eigenschaft
|
pg_repack
|
PostgreSQL 19
|
|---|---|---|
|
Bestandteil
|
externe Extension
|
PostgreSQL Core
|
|
Installation
|
zusätzlich notwendig
|
nicht notwendig
|
|
Bloat beseitigen
|
ja
|
ja
|
|
Tabelle neu schreiben
|
ja
|
ja
|
|
Online-Variante
|
ja
|
REPACK (CONCURRENTLY)
|
|
parallele DML erfassen
|
Trigger + Log-Tabelle
|
Logical Decoding
|
|
finaler exklusiver Lock
|
ja
|
ja
|
|
physische Relation wird ersetzt
|
ja
|
ja
|
|
Core Progress View
|
nein
|
pg_stat_progress_repack
|
Der Praxistest zeigt ziemlich deutlich, welchen Mehrwert REPACK für den PostgreSQL-Betrieb bringen kann. Nach dem Löschen von 50 Prozent der Datensätze blieb die physische Größe der getesteten Relation zunächst unverändert. Auch ein normales VACUUM reduzierte den belegten Speicher nicht. Erst REPACK schrieb die Relation vollständig neu und verringerte die Gesamtgröße im Test von rund 1.495 MB auf 748 MB – also nahezu entsprechend der zuvor halbierten Datenmenge.
Für DBAs und IT-Administratoren bedeutet das: Bloat lässt sich mit PostgreSQL 19 künftig mit einem nativen Core-Befehl nicht nur logisch bereinigen, sondern physisch aus der Datenbank entfernen. Besonders bei großen, über längere Zeit gewachsenen Tabellen kann das einen spürbaren Unterschied machen.
Und der Effekt endet nicht an der Datenbankgrenze. Wenn unnötig belegter Speicher tatsächlich an das Betriebssystem zurückgegeben wird, kann – je nach Anwendungsfall – auch der physische Storage-Bedarf der Datenbank sinken. In größeren Umgebungen kann das unter Umständen vorhandene Speicherkapazitäten besser ausnutzen und den Storage-Bedarf optimieren.
Mit REPACK (CONCURRENTLY) liefert PostgreSQL 19 außerdem eine weitgehend online nutzbare Variante für Systeme, bei denen ein länger blockierender Rewrite betrieblich kaum möglich ist. Allerdings sollten DBAs dabei die beschriebenen Einschränkungen bei langen beziehungsweise alten MVCC-Snapshots im Blick behalten.
Unterm Strich wird REPACK damit zu einem spannenden neuen Werkzeug für alle, die PostgreSQL-Datenbanken nicht nur logisch sauber, sondern auch beim tatsächlichen Ressourcenverbrauch effizient betreiben wollen.
Bitte beachte, dass REPACK Bestandteil der » PostgreSQL Version 19 ist, welche aktuell nur in der Beta 3 verfügbar ist. Wir informieren dich, sobald die Version 19 final veröffentlicht wurde.
Hier findest du weitere Infos aus der Welt von PostgreSQL aus unserem News & Insights Bereich.
Share this article
Sie sehen gerade einen Platzhalterinhalt von YouTube. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr InformationenSie müssen den Inhalt von reCAPTCHA laden, um das Formular abzuschicken. Bitte beachten Sie, dass dabei Daten mit Drittanbietern ausgetauscht werden.
Mehr InformationenSie sehen gerade einen Platzhalterinhalt von Facebook. Um auf den eigentlichen Inhalt zuzugreifen, klicken Sie auf die Schaltfläche unten. Bitte beachten Sie, dass dabei Daten an Drittanbieter weitergegeben werden.
Mehr Informationen