05 Dezember 2025

Ein tieferer Blick auf alte UUIDv4 vs. neue UUIDv7 in PostgreSQL 18

In der Vergangenheit gab es viele Diskussionen über die Verwendung von UUID als Primärschlüssel in PostgreSQL. Für einige Anwendungen reicht selbst eine BIGINT-Spalte nicht aus: Es handelt sich um eine vorzeichenbehaftete 8-Byte-Ganzzahl mit einem Bereich von −9.223.372.036.854.775.808 bis +9.223.372.036.854.775.807. Obwohl diese Werte groß genug erscheinen, wird diese Zahl weniger beeindruckend, wenn wir an Webdienste denken, die täglich Milliarden oder mehr Datensätze sammeln. Einfache Ganzzahlwerte können auch zu Wertkonflikten in verteilten Systemen, in Data Lakehouses beim kombinieren von Daten aus mehreren Quelldatenbanken usw. führen.

Das Hauptproblem bei UUIDv4 als Primärschlüssel in PostgreSQL war jedoch nicht der fehlende Bereich, sondern die vollständige Zufälligkeit der Werte. Diese Zufälligkeit führt zu häufigen B-Baum-Seitenaufteilungen, einem stark fragmentierten Primärschlüsselindex und somit zu vielen zufälligen Festplatten-I/Os. Es gab bereits viele Artikel und Konferenzvorträge, die dieses Problem beschrieben haben. Was viele dieser Ressourcen jedoch nicht taten, war, tief in die On-Disk-Strukturen einzutauchen. Das wollte ich hier untersuchen.

Was sind UUIDs

UUID (Universally Unique Identifier) ist ein 16-Byte-Ganzzahlwert (128 Bit), der 2^128 mögliche Kombinationen aufweist (ungefähr 3,4 × 10^38). Dieser Bereich ist so groß, dass für die meisten Anwendungen die Wahrscheinlichkeit einer doppelten UUID praktisch null ist. Wikipedia zeigt eine Berechnung, die demonstriert, dass die Wahrscheinlichkeit, ein Duplikat innerhalb von 103 Billionen Version-4-UUIDs zu finden, etwa eins zu einer Milliarde beträgt. Eine weitere oft zitierte Faustregel besagt, dass man, um eine 50%ige Chance auf eine Kollision zu haben, etwa 1 Milliarde UUIDs pro Sekunde über etwa 86 Jahre generieren müsste.

Werte werden üblicherweise als 36-stelliger String mit Hexadezimalziffern und Bindestrichen dargestellt, zum Beispiel: f47ac10b-58cc-4372-a567-0e02b2c3d479. Das kanonische Layout ist 8-4-4-4-12 Zeichen. Das erste Zeichen im dritten Block und das erste Zeichen im vierten Block haben eine besondere Bedeutung: xxxxxxxx-xxxx-Vxxx-Wxxx-xxxxxxxxxxxxV kennzeichnet die UUID-Version (4 für UUIDv4, 7 für UUIDv7 usw.), W kodiert die Variante in ihren oberen 2 oder 3 Bits (die Layout-Familie der UUID).

Bis PostgreSQL 18 war die gängige Methode zur Generierung von UUIDs in PostgreSQL die Verwendung von Version 4 (zum Beispiel über gen_random_uuid() oder uuid_generate_v4() aus Erweiterungen). PostgreSQL 18 führt native Unterstützung für die neue zeitlich geordnete UUIDv7 über die Funktion uuidv7() ein und fügt auch uuidv4() als integrierten Alias für die ältere Funktion gen_random_uuid() hinzu. UUID Version 4 wird vollständig zufällig generiert (abgesehen von den festen Versions- und Variantenbits), sodass es keine inhärente Reihenfolge in den Werten gibt. UUID Version 7 generiert Werte, die zeitlich geordnet sind, da die ersten 48 Bits einen Big-Endian Unix-Epoch-Zeitstempel mit ungefähr Millisekunden-Granularität enthalten, gefolgt von zusätzlichen Sub-Millisekunden-Bits und Zufälligkeit.

Slonik, das PostgreSQL-Elefantenlogo

Testaufbau in PostgreSQL 18

Ich werde konkrete Ergebnisse anhand eines einfachen Testaufbaus zeigen – 2 verschiedene Tabellen mit der Spalte „id“, die einen generierten UUID-Wert (entweder v4 oder v7) enthält, der als Primärschlüssel verwendet wird, und der Spalte „ord“ mit sequenziell generiertem Bigint, wobei die Reihenfolge der Zeilenerstellung beibehalten wird.

-- UUIDv4 (completely random keys)
CREATE TABLE uuidv4_demo (
id uuid PRIMARY KEY DEFAULT uuidv4(), -- alias of gen_random_uuid()
ord bigint GENERATED ALWAYS AS IDENTITY
);

-- UUIDv7 (time-ordered keys)
CREATE TABLE uuidv7_demo (
id uuid PRIMARY KEY DEFAULT uuidv7(),
ord bigint GENERATED ALWAYS AS IDENTITY
);

-- 1M rows with UUIDv4
INSERT INTO uuidv4_demo (id) SELECT uuidv4() FROM generate_series(1, 1000000);

-- 1M rows with UUIDv7
INSERT INTO uuidv7_demo (id) SELECT uuidv7() FROM generate_series(1, 1000000);

VACUUM ANALYZE uuidv4_demo;
VACUUM ANALYZE uuidv7_demo;

Performance auf Abfrageebene: EXPLAIN ANALYZE

Als ersten Schritt vergleichen wir die Kosten der Sortierung nach UUID für die beiden Tabellen:

-- UUIDv4
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM uuidv4_demo ORDER BY id;

Index Scan using uuidv4_demo_pkey on uuidv4_demo (cost=0.42..60024.31 rows=1000000 width=24) (actual time=0.031..301.163 rows=1000000.00 loops=1)
Index Searches: 1
Buffers: shared hit=1004700 read=30
Planning Time: 0.109 ms
Execution Time: 318.005 ms

-- UUIDv7
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM uuidv7_demo ORDER BY id;

Index Scan using uuidv7_demo_pkey on uuidv7_demo (cost=0.42..36785.43 rows=1000000 width=24) (actual time=0.013..96.177 rows=1000000.00 loops=1)
Index Searches: 1
Buffers: shared hit=2821 read=7383
Planning Time: 0.040 ms
Execution Time: 113.305 ms

Die genauen Pufferzahlen hängen von Cache-Effekten ab, aber eines ist in diesem Durchlauf klar: Der Index-Scan über UUIDv7 benötigt etwa 100-mal weniger Puffer-Treffer und ist etwa dreimal schneller (113 ms vs. 318 ms) für dieselbe Million-Zeilen-ORDER BY id. Dies ist das erste Anzeichen dafür, dass UUIDv7 eine sehr praktikable Lösung für einen Primärschlüssel ist, wenn wir eine BIGINT-Spalte durch etwas ersetzen müssen, das einen viel größeren Speicherplatz und eine höhere Einzigartigkeit bietet, während es sich aus Sicht des Index immer noch wie ein sequenzieller Schlüssel verhält.

Einfügegeschwindigkeit – einfaches Benchmarking

Ursprünglich wollte ich ausgefeiltere Tests durchführen, aber selbst ein sehr einfacher, naiver Benchmark zeigte einen enormen Unterschied in der Einfügegeschwindigkeit. Ich verglich die Zeit, die benötigt wurde, um 50 Millionen Zeilen in eine leere Tabelle einzufügen, und dann noch einmal in eine Tabelle mit 50 Millionen vorhandenen Zeilen.

INSERT INTO uuidv4_demo (id) SELECT uuidv4() FROM generate_series(1, 50000000);
INSERT INTO uuidv7_demo (id) SELECT uuidv7() FROM generate_series(1, 50000000);

-- UUID v4 -- UUID v7
Empty table
Insert time: 1239839.702 ms (20:39.840) Insert time: 106343.314 ms (01:46.343)
Table size: 2489 MB Table size: 2489 MB
Index size: 1981 MB Index size: 1504 MB

Table with 50M rows
Insert time: 2776880.790 ms (46:16.881) Insert time: 100354.087 ms (01:40.354)
Table size: 4978 MB Table size: 4978 MB
Index size: 3956 MB Index size: 3008 MB

Wie wir sehen können, ist die Einfügegeschwindigkeit radikal unterschiedlich. Das Einfügen der ersten 50 Millionen Zeilen in eine leere Tabelle dauerte für UUIDv7 nur 1:46 Minuten, aber bereits 20 Minuten für UUIDv4. Die zweite Charge zeigte sogar einen 2-mal größeren Unterschied.

Wie Werte in der Tabelle verteilt sind

Diese Ergebnisse deuten auf enorme Unterschiede in den Indizes hin. Lassen Sie uns dies analysieren. Zuerst werden wir überprüfen, wie die Werte in der Tabelle verteilt sind. Ich verwende die folgende Abfrage für beide Tabellen (nur den Tabellennamen wechseln):

SELECT
row_number() OVER () AS seq_in_uuid_order,
id,
ord,
ctid
FROM uuidv4_demo
ORDER BY id
LIMIT 20;

Die Spalte seq_in_uuid_order ist lediglich die Zeilennummer in UUID-Reihenfolge, ord ist die Einfügereihenfolge, ctid zeigt den physischen Speicherort jedes Tupels im Heap an: (block_number, offset_in_block).

UUIDv4: random UUID order ⇒ random heap access

Wie sehen die Ergebnisse für UUIDv4 aus?

 seq_in_uuid_order | id                                   | ord    | ctid 
-------------------+--------------------------------------+--------+------------
1 | 00000abf-cc8e-4cb2-a91a-701a3c96bd36 | 673969 | (4292,125)
2 | 00001827-16fe-4aee-9bce-d30ca49ceb1d | 477118 | (3038,152)
3 | 00001a84-6d30-492f-866d-72c3b4e1edff | 815025 | (5191,38)
4 | 00002759-21d1-4889-9874-4a0099c75286 | 879671 | (5602,157)
5 | 00002b44-b1b5-473f-b63f-7554fa88018d | 729197 | (4644,89)
6 | 00002ceb-5332-44f4-a83b-fb8e9ba73599 | 797950 | (5082,76)
7 | 000040e2-f6ac-4b5e-870a-63ab04a5fa39 | 160314 | (1021,17)
8 | 000053d7-8450-4255-b320-fee8d6246c5b | 369644 | (2354,66)
9 | 00009c78-6eac-4210-baa9-45b835749838 | 463430 | (2951,123)
10 | 0000a118-f98e-4e4a-acb3-392006bcabb8 | 96325 | (613,84)
11 | 0000be99-344b-4529-aa4c-579104439b38 | 454804 | (2896,132)
12 | 00010300-fcc1-4ec4-ae16-110f93023068 | 52423 | (333,142)
13 | 00010c33-a4c9-4612-ba9a-6c5612fe44e6 | 82935 | (528,39)
14 | 00011fa2-32ce-4ee0-904a-13991d451934 | 988370 | (6295,55)
15 | 00012920-38c7-4371-bd15-72e2996af84d | 960556 | (6118,30)
16 | 00014240-7228-4998-87c1-e8b23b01194a | 66048 | (420,108)
17 | 00014423-15fc-42ca-89bd-1d0acf3e5ad2 | 250698 | (1596,126)
18 | 000160b9-a1d8-4ef0-8979-8640025c0406 | 106463 | (678,17)
19 | 0001711a-9656-4628-9d0c-1fb40620ba41 | 920459 | (5862,125)
20 | 000181d5-ee13-42c7-a9e7-0f2c52faeadb | 513817 | (3272,113)

Die Werte sind vollständig zufällig verteilt. Das Lesen von Zeilen in UUID-Reihenfolge ist hier praktisch sinnlos und führt direkt zu zufälligen Heap-Zugriffen bei Abfragen, die den Primärschlüsselindex verwenden.

UUIDv7: UUID-Reihenfolge folgt der Einfügereihenfolge

Andererseits werden UUIDv7-Werte in einer klaren Reihenfolge generiert:

 seq_in_uuid_order | id                                   | ord | ctid  
-------------------+--------------------------------------+-----+--------
1 | 019ad94d-0127-7aba-b9f6-18620afdea4a | 1 | (0,1)
2 | 019ad94d-0131-72b9-823e-89e41d1fad73 | 2 | (0,2)
3 | 019ad94d-0131-7384-b03d-8820be60f88e | 3 | (0,3)
4 | 019ad94d-0131-738b-b3c0-3f91a0b223a8 | 4 | (0,4)
5 | 019ad94d-0131-7391-ab84-a719ca98accf | 5 | (0,5)
6 | 019ad94d-0131-7396-b41d-7f9f27a179c4 | 6 | (0,6)
7 | 019ad94d-0131-739b-bdb3-4659aeaafbdd | 7 | (0,7)
8 | 019ad94d-0131-73a0-b271-7dba06512231 | 8 | (0,8)
9 | 019ad94d-0131-73a5-8911-5ec5d446c8a9 | 9 | (0,9)
10 | 019ad94d-0131-73aa-a4a3-0e5c14f09374 | 10 | (0,10)
11 | 019ad94d-0131-73af-ac4b-3710e221390e | 11 | (0,11)
12 | 019ad94d-0131-73b4-85d6-ed575d11e9cf | 12 | (0,12)
13 | 019ad94d-0131-73b9-b802-d5695f5bf781 | 13 | (0,13)
14 | 019ad94d-0131-73be-bcb0-b0775dab6dd4 | 14 | (0,14)
15 | 019ad94d-0131-73c3-9ec8-c7400b5c8983 | 15 | (0,15)
16 | 019ad94d-0131-73c8-b067-435258087b3a | 16 | (0,16)
17 | 019ad94d-0131-73cd-a03f-a28092604fb1 | 17 | (0,17)
18 | 019ad94d-0131-73d3-b4d5-02516d5667b5 | 18 | (0,18)
19 | 019ad94d-0131-73d8-9c41-86fa79f74673 | 19 | (0,19)
20 | 019ad94d-0131-73dd-b9f1-dcd07598c35d | 20 | (0,20)

Hier folgen seq_in_uuid_order, ord und ctid alle schön aufeinander – ord erhöht sich für jede Zeile um 1, ctid bewegt sich sequenziell durch die erste Heap-Seite, und die UUIDs selbst sind aufgrund des Zeitstempelpräfixes monoton. Für Index-Scans auf dem Primärschlüssel bedeutet dies, dass Postgres den Heap viel sequenzieller durchlaufen kann als mit UUIDv4.

Wie sequenziell sind diese Werte statistisch?

Nach VACUUM ANALYZE frage ich den Planer, was er über die Korrelation zwischen ID und der physischen Reihenfolge denkt:

SELECT
tablename,
attname,
correlation
FROM pg_stats
WHERE tablename IN ('uuidv4_demo', 'uuidv7_demo')
AND attname = 'id'
ORDER BY tablename, attname;

Ergebnis:

 tablename   | attname | correlation 
-------------+---------+---------------
uuidv4_demo | id | -0.0024808696
uuidv7_demo | id | 1

Die Statistiken bestätigen, was wir gerade gesehen haben:

  • Für uuidv4_demo.id ist die Korrelation im Wesentlichen 0 ⇒ Werte sind zufällig in Bezug auf die Heap-Reihenfolge.
  • Für uuidv7_demo.id ist die Korrelation 1 ⇒ perfekte Übereinstimmung zwischen UUID-Reihenfolge und physischer Zeilenreihenfolge in diesem Testlauf.

Diese hohe Korrelation ist genau der Grund, warum UUIDv7 als Primärschlüssel für B-Baum-Indizes so attraktiv ist.

Primärschlüsselindizes: Größe, Leaf Pages, Dichte, Fragmentierung

Als Nächstes betrachte ich die Primärschlüsselindizes – ihre Größe, Anzahl der Leaf Pages, Dichte und Fragmentierung – mithilfe von pgstatindex:

SELECT 'uuidv4_demo_pkey' AS index_name, (pgstatindex('uuidv4_demo_pkey')).*;

index_name | uuidv4_demo_pkey
version | 4
tree_level | 2
index_size | 40026112
root_block_no. | 295
internal_pages | 24
leaf_pages. | 4861
empty_pages | 0
deleted_pages | 0
avg_leaf_density | 71
leaf_fragmentation | 49.99

SELECT 'uuidv7_demo_pkey' AS index_name, (pgstatindex('uuidv7_demo_pkey')).*;

index_name | uuidv7_demo_pkey
version | 4
tree_level | 2
index_size | 31563776
root_block_no | 295
internal_pages. | 20
leaf_pages | 3832
empty_pages | 0
deleted_pages | 0
avg_leaf_density | 89.98 -- i.e. standard 90% fillfactor
leaf_fragmentation | 0

Wir können sofort erkennen, dass der Primärschlüsselindex auf UUIDv4 etwa 26–27 % größer ist:

  • index_size beträgt ~40 MB vs. ~31,6 MB
  • leaf_pages sind 4861 vs. 3832 (wiederum etwa 26–27 % mehr)
  • Leaf Pages im v4-Index haben eine geringere durchschnittliche Dichte (71 vs. ~90)
  • leaf_fragmentation für v4 beträgt etwa 50 %, während sie für v7 0 ist

UUIDv4 zwingt den B-Baum also dazu, mehr Pages zu allozieren und diese weniger voll zu halten, und fragmentiert die Blattebene wesentlich stärker.

Tiefere Indexanalyse mit bt_multi_page_stats

Um tiefer einzusteigen, habe ich die B-Baum-Indizes Seite für Seite untersucht und einige Statistiken erstellt. Ich habe die folgende Abfrage für beide Indizes verwendet (nur den Indexnamen im CTE ändern). Die Abfrage berechnet die minimale, maximale und durchschnittliche Anzahl von Tupeln pro Index-Pages und überprüft auch, wie sequenziell Pages in der Indexdatei gespeichert sind:

WITH leaf AS (
 SELECT *
 FROM bt_multi_page_stats('uuidv4_demo_pkey', 1, -1) -- from block 1 to end
 WHERE type = 'l'
)
SELECT
 count(*) AS leaf_pages,
 min(blkno) AS first_leaf_blk,
 max(blkno) AS last_leaf_blk,
 max(blkno) - min(blkno) + 1 AS leaf_span,
 round( count(*)::numeric / (max(blkno) - min(blkno) + 1), 3) AS leaf_density_by_span,
 min(live_items) AS min_tuples_per_page,
 max(live_items) AS max_tuples_per_page,
 avg(live_items)::numeric(10,2) AS avg_tuples_per_page,
 sum(CASE WHEN btpo_next = blkno + 1 THEN 1 ELSE 0 END) AS contiguous_links,
 sum(CASE WHEN btpo_next  0 AND btpo_next  blkno + 1 THEN 1 ELSE 0 END) AS non_contiguous_links
FROM leaf;

Ergebnisse für UUIDv4:

-- uuidv4_demo_pkey
leaf_pages | 4861
first_leaf_blk | 1
last_leaf_blk | 4885
leaf_span | 4885
leaf_density_by_span | 0.995
min_tuples_per_page | 146
max_tuples_per_page | 291
avg_tuples_per_page | 206.72
contiguous_links | 0
non_contiguous_links | 4860

Ergebnisse für UUIDv7:

-- uuidv7_demo_pkey
leaf_pages | 3832
first_leaf_blk | 1
last_leaf_blk | 3852
leaf_span | 3852
leaf_density_by_span | 0.995
min_tuples_per_page | 109
max_tuples_per_page | 262
avg_tuples_per_page | 261.96
contiguous_links | 3812
non_contiguous_links | 19

Wie wir sehen können, hat der UUIDv4-Index mehr Pages, die sich über einen größeren Bereich von Blöcken verteilen, und obwohl er eine höhere minimale und maximale Anzahl von Tupeln pro Seite aufweist, ist seine durchschnittliche Anzahl von Tupeln pro Page (206,72) signifikant niedriger als für UUIDv7 (261,96).

Diese Zahlen können jedoch das Gesamtbild verschleiern. Schauen wir uns also Histogramme an, die die Anzahl der Tupel in Pages visualisieren. Dafür werde ich die folgende Abfrage mit Buckets zwischen 100 und 300 verwenden und nur nicht-leere Ergebnisse auflisten:

WITH leaf AS (
 SELECT live_items
 FROM bt_multi_page_stats('uuidv4_demo_pkey', 1, -1)
  WHERE type = 'l'
),
buckets AS (
 -- bucket lower bounds: 100, 110, ..., 290
 SELECT generate_series(100, 290, 10) AS bucket_min
)
SELECT
 b.bucket_min AS bucket_from,
 b.bucket_min + 9 AS bucket_to,
 COUNT(l.live_items) AS page_count
FROM buckets b
LEFT JOIN leaf l
 ON l.live_items BETWEEN b.bucket_min AND b.bucket_min + 9
GROUP BY b.bucket_min HAVING count(l.live_items) > 0
ORDER BY b.bucket_min;

Ergebnis für UUIDv4:

 bucket_from | bucket_to | page_count 
-------------+-----------+------------
140 | 149 | 159
150 | 159 | 435
160 | 169 | 388
170 | 179 | 390
180 | 189 | 427
190 | 199 | 466
200 | 209 | 430
210 | 219 | 387
220 | 229 | 416
230 | 239 | 293
240 | 249 | 296
250 | 259 | 228
260 | 269 | 214
270 | 279 | 171
280 | 289 | 140
290 | 299 | 21

Ergebnis für UUIDv7:

 bucket_from | bucket_to | page_count 
-------------+-----------+------------
100 | 109 | 1
260 | 269 | 3831

Diese Ergebnisse demonstrieren eindrucksvoll die enorme Fragmentierung des UUIDv4-Index und die stabile, kompakte Struktur des UUIDv7-Index. Die niedrigsten Buckets im UUIDv4-Histogramm zeigen Fälle von halb leeren Blattindexseiten (leaf index pages), andererseits überschreiten Seiten mit mehr als 270 Tupeln den 90 % Füllfaktor, da PostgreSQL den verbleibenden freien Speicherplatz nutzt, um Splits zu vermeiden. Im UUIDv7-Index sind alle Pages bis auf eine (die allerletzte im Baum) bis zum 90 % Standard-Füllfaktor gefüllt.

Ein weiteres wichtiges Ergebnis findet sich in den letzten beiden Spalten der Indexstatistiken:

  • Für UUIDv4: contiguous_links = 0, non_contiguous_links = 4860
  • Für UUIDv7: contiguous_links = 3812, non_contiguous_links = 19

btpo_next = blkno + 1 bedeutet, dass die nächste Page in der logischen B-Baum-Reihenfolge auch der nächste physische Block ist. Bei UUIDv4 geschieht dies in diesem Test nie – die Page sind vollständig fragmentiert und zufällig über die Indexstruktur verteilt. Bei UUIDv7 sind fast alle Pages zusammenhängend, d.h. sie folgen schön aufeinander.

Wenn wir den tatsächlichen Inhalt der Pages untersuchen, können wir sofort die Zufälligkeit von UUIDv4 im Vergleich zum sequenziellen Verhalten von UUIDv7 erkennen: UUIDv4-Pages verweisen auf Heap-Tupel, die über die gesamte Tabelle verstreut sind, während UUIDv7-Pages dazu neigen, auf enge Bereiche von Heap-Seiten zu verweisen. Das Ergebnis ist dasselbe Muster, das wir zuvor beim direkten Betrachten von ctid aus der Tabelle gesehen haben, daher werde ich die Roh-Dumps hier nicht wiederholen.

Ein kleiner Haken: eingebetteter Zeitstempel in UUIDv7

Es gibt einen kleinen Haken bei UUIDv7-Werten: Sie legen einen Zeitstempel der Erstellung offen. PostgreSQL 18 macht dies explizit über uuid_extract_timestamp():

SELECT 
id,
uuid_extract_timestamp(id) AS created_at_from_uuid
FROM uuidv7_demo
ORDER BY ord
LIMIT 5;

Beispielergebnisse:

 id                                   | created_at_from_uuid 
--------------------------------------+----------------------------
019ad94d-0127-7aba-b9f6-18620afdea4a | 2025-12-01 09:44:53.799+00
019ad94d-0131-72b9-823e-89e41d1fad73 | 2025-12-01 09:44:53.809+00
019ad94d-0131-7384-b03d-8820be60f88e | 2025-12-01 09:44:53.809+00
019ad94d-0131-738b-b3c0-3f91a0b223a8 | 2025-12-01 09:44:53.809+00
019ad94d-0131-7391-ab84-a719ca98accf | 2025-12-01 09:44:53.809+00

Betrachten wir die gesamte Wertefolge, können wir die Zeitdifferenzen zwischen den Datensatz-Erstellungen direkt aus den UUIDs analysieren, ohne eine separate Zeitstempelspalte. Für einige Anwendungen könnte dies als potenzielles Informationsleck angesehen werden (z. B. die Offenlegung ungefährer Erstellungszeiten oder Anfrageraten), während es vielen anderen höchstwahrscheinlich egal sein wird.

Zusammenfassung

  • UUIDs bieten einen enormen Bezeichnerraum (128 Bit, ~3,4 × 10^38 Werte), bei dem die Wahrscheinlichkeit einer Kollision für reale Workloads vernachlässigbar ist.
  • Traditionelle UUIDv4-Schlüssel sind vollständig zufällig. Wenn sie als Primärschlüssel in PostgreSQL verwendet werden, neigen sie dazu:
    • B-Baum-Indizes zu fragmentieren
    • die Dichte der Pages zu verringern
    • stark zufällige Heap-Zugriffsmuster und mehr zufällige I/O zu verursachen
  • UUIDv7, nativ in PostgreSQL 18 als uuidv7() eingeführt, behält den 128-Bit-Raum bei, ordnet die Bits jedoch so neu an, dass:
    • die höchstwertigen Bits einen Unix-Zeitstempel mit Millisekundenpräzision (plus Sub-Millisekunden-Bruchteil) enthalten
    • die restlichen Bits zufällig bleiben
  • In praktischen Tests mit 1 Million Zeilen pro Tabelle:
    • Der UUIDv7-Primärschlüsselindex war etwa 26–27 % kleiner, mit weniger Pages und einer viel höheren durchschnittlichen Blattdichte
    • Pages im UUIDv7-Index waren überwiegend physisch zusammenhängend, während die UUIDv4-Pages vollständig fragmentiert waren
    • Eine ORDER BY id-Abfrage über UUIDv7 war in meinem Durchlauf etwa dreimal schneller als dieselbe Abfrage über UUIDv4, dank besserer Indexlokalität und sequenziellerem Heap-Zugriff

Der Kompromiss besteht darin, dass UUIDv7 einen Zeitstempel einbettet, der ungefähre Erstellungszeiten offenlegen könnte, aber für die meisten Anwendungsfälle ist dies akzeptabel oder sogar nützlich. UUIDv7 verbessert also die Leistung und das physische Layout von UUID-Primärschlüsseln in PostgreSQL erheblich, nicht indem es die Zufälligkeit aufgibt, sondern indem es ein zeitlich geordnetes Präfix hinzufügt. In PostgreSQL 18 bietet uns das das Beste aus beiden Welten: den riesigen Bezeichnerraum und die Vorteile der verteilten Generierung von UUIDs, mit einem Indexverhalten, das einem klassischen sequenziellen BIGINT-Primärschlüssel viel näher kommt.


PostgreSQL ist eine Open-Source-Datenbank, die von den PostgreSQL-Entwicklern bereitgestellt wird. Das PostgreSQL-Elefantenlogo („Slonik“), Postgres und PostgreSQL sind eingetragene Marken der PostgreSQL Community Association.

Wir bei credativ bieten umfassende Support- und Beratungsleistungen für den Betrieb von PostgreSQL und anderen Open-Source-Systemen an.

Kategorien: PostgreSQL®
Tags: planetpostgres planetpostgresql PostgreSQL® UUID UUIDv7

JM

über den Autor

Josef Machytka


Beitrag teilen: