backend

Postgres 18 UUIDv7 Primary Keys: Practical Guide (2026)

June 17, 2026

Postgres 18 UUIDv7 Primary Keys: Practical Guide (2026)

PostgreSQL 18 adds a built-in uuidv7() function, so you can use uuid PRIMARY KEY DEFAULT uuidv7() with no extension. UUIDv7 embeds a millisecond timestamp in its leading bits, so values sort by creation time and behave far more like a sequential key than random UUIDv4.12

TL;DR

PostgreSQL 18 (current stable 18.4)3 ships native uuidv7(), uuidv4() (an alias for gen_random_uuid()), and updated uuid_extract_timestamp() / uuid_extract_version() helpers.1 For a new table, just write id uuid PRIMARY KEY DEFAULT uuidv7(). UUIDv7's time-ordered prefix gives smaller, less-fragmented B-tree indexes than UUIDv4 — in one published benchmark the primary-key index was about 26–27% smaller and ordered scans ran roughly three times faster.2 The one real trade-off: a UUIDv7 leaks the row's approximate creation time. Every SQL block below was run against PostgreSQL 18.3 before publishing.

What you'll learn

  • Whether PostgreSQL 18 has a native UUIDv7 function and what shipped with it
  • How to set uuidv7() as a default primary key with copy-paste DDL
  • How UUIDv7 compares to UUIDv4 on index size, fragmentation, and speed
  • When to choose UUIDv7 over a plain bigint identity column
  • How to migrate an existing UUIDv4 table to UUIDv7 without losing order
  • How to read the creation time back out of a UUIDv7
  • Why a UUIDv7 leaks creation time and how to mitigate it

Does PostgreSQL 18 have a built-in UUIDv7 function?

Yes. PostgreSQL 18 added uuidv7() as a core function — no uuid-ossp or pgcrypto extension required.1 It generates a version 7 (time-ordered) UUID per RFC 9562, with the timestamp built from a UNIX millisecond value plus a sub-millisecond fraction plus random bits.1

The same release rounded out the UUID toolbox. gen_random_uuid() (version 4, fully random) is still there, and uuidv4() was added as a built-in alias for it so the naming matches uuidv7().1 Two extraction helpers were updated to understand v7: uuid_extract_version(uuid) returns the version number, and uuid_extract_timestamp(uuid) returns a timestamp with time zone for version 1 or 7 UUIDs (and null for others).1

SELECT
  uuid_extract_version(gen_random_uuid()) AS v4,  -- 4
  uuid_extract_version(uuidv7())          AS v7;  -- 7

UUIDv7 was added natively in 18; on PostgreSQL 17 and earlier you still need an extension or application-side library to generate it.1

How do I use uuidv7() as a default primary key?

Set the column default to uuidv7(). Because the function runs server-side on every insert, you never touch the id in application code:

CREATE TABLE invoices (
    id         uuid PRIMARY KEY DEFAULT uuidv7(),
    amount     numeric,
    created_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO invoices (amount)
SELECT g FROM generate_series(1, 5) AS g;

SELECT id, amount FROM invoices ORDER BY id;

Run against PostgreSQL 18.3, the rows come back in insertion order because the IDs are monotonic:

                  id                  | amount
--------------------------------------+--------
 019ed46f-1d7f-7000-9fd1-6978e2d1412f |      1
 019ed46f-1d7f-7001-924f-a07704cd74c2 |      2
 019ed46f-1d7f-7002-817a-caaa62db219e |      3
 019ed46f-1d7f-7003-b9c9-fc99d1d64969 |      4
 019ed46f-1d7f-7004-abf1-966ff34e4974 |      5

That ordering is not luck. PostgreSQL stores a 12-bit sub-millisecond fraction right after the 48-bit timestamp and uses the RFC 9562 "replace leftmost random bits with increased clock precision" method, which guarantees monotonically increasing values for every UUIDv7 generated within the same session (backend process). Ordering is not guaranteed across different backends.4

Is UUIDv7 better than UUIDv4 for primary keys?

For index health, yes — and that is the whole point. UUIDv4 is random, so each insert lands at a random spot in the primary-key B-tree, causing page splits, low page density, and fragmentation. UUIDv7 inserts land at the right-hand edge of the index, like a sequential key.

A detailed PostgreSQL 18 benchmark by credativ (1M and 50M rows) measured the difference on real on-disk structures:2

Metric (1M rows)UUIDv4UUIDv7
Primary-key index size~40 MB~31.6 MB (~26–27% smaller)
Leaf pages48613832
Average leaf density71%~90%
Leaf fragmentation~50%0%
id vs heap-order correlation-0.00251.0
ORDER BY id over 1M rows~318 ms~113 ms

In that same test, inserting 50M rows into an empty table took about 20 minutes for UUIDv4 versus under 2 minutes for UUIDv7, and the gap widened on a second batch.2 Exact numbers depend on hardware, caching, and workload, but the direction is consistent: the time-ordered prefix keeps the index compact and inserts cheap. Note that the heap table size is identical — a UUID is 16 bytes either way; only the index layout changes.2

UUIDv7 vs bigint: which primary key should you use?

Use a bigint identity column when you are on a single node and want the smallest, simplest key; use UUIDv7 when you need globally unique, client- or distributed-generatable IDs without giving up index locality.5

A bigint is 8 bytes against a UUID's 16, so a bigint index entry is half the size — and because that key is copied into every secondary index and foreign key, the saving compounds across a schema — while a signed bigint still counts to about 9.2 × 10¹⁸ before overflowing.2 What bigint sequences cannot do is let many writers (microservices, mobile clients, sharded nodes) mint non-colliding keys independently, or hide the table's row count — a sequence value is guessable and reveals volume.5

UUIDv7 is the middle ground: it keeps the enormous 128-bit keyspace and distributed-generation property of UUIDs, while its index behaves close to a sequential bigint instead of the random mess UUIDv4 produces.2 For a brand-new single-server CRUD app, bigint identity is still a perfectly good default. The moment you anticipate distributed writes, data merges, or exposing IDs across services, UUIDv7 is the stronger primary key.

How do I migrate an existing UUIDv4 table to UUIDv7?

Changing a live primary key is a heavy operation — you add a new column, backfill it, then move constraints and foreign keys over — so plan it like any schema migration. The mechanical part is adding a v7 column that defaults uuidv7() for new rows and backfilling old rows in a way that preserves their original order:

-- New rows get a real UUIDv7; existing rows are filled next.
ALTER TABLE legacy
  ADD COLUMN id_v7 uuid NOT NULL DEFAULT uuidv7();

-- Backfill old rows so the embedded timestamp matches each row's created_at.
UPDATE legacy
SET id_v7 = uuidv7(created_at - now());

The key detail is the uuidv7(shift interval) argument. uuidv7() with no argument embeds the current time; the optional shift offsets that time by an interval.1 To make a backfilled UUID carry a row's original created_at, the shift must be created_at - now(). Verified against PostgreSQL 18.3, the round-trip is exact to the millisecond:

SELECT created_at,
       uuid_extract_timestamp(uuidv7(created_at - now())) AS embedded
FROM legacy ORDER BY created_at;
       created_at       |        embedded
------------------------+------------------------
 2024-03-01 12:00:00+00 | 2024-03-01 12:00:00+00
 2024-06-15 08:30:00+00 | 2024-06-15 08:30:00+00
 2025-01-20 23:15:00+00 | 2025-01-20 23:15:00+00

Watch out for a formula copied around the web: uuidv7((created_at - '2025-01-01')::interval). It looks plausible but does not embed created_at — it offsets now by the distance from a fixed anchor date, landing the timestamp years away from the real value. The same verified run shows a 2024-03-01 row coming back as 2025-08-15. Always shift relative to now(). Also remember that any backfilled ID is synthetic: it reproduces ordering, not the genuine generation instant, which never existed for those legacy rows.

Can I extract the creation time from a UUIDv7?

Yes — uuid_extract_timestamp() reads the embedded timestamp back out as a timestamp with time zone:

SELECT id, uuid_extract_timestamp(id) AS created_at_from_uuid
FROM invoices ORDER BY id LIMIT 1;

For a version 4 UUID the same function returns null, because there is no timestamp to recover.1 The docs note the extracted value "is not necessarily exactly equal to the time the UUID was generated" — it reflects whatever the generator wrote — but for Postgres's own uuidv7() it is the insert-time clock reading.1 This makes UUIDv7 handy for keyset pagination, log correlation, and time-series style scans where the key itself tells you roughly when a row was created.2

Does a UUIDv7 leak the row's creation time?

Yes, and it is the one caveat worth designing around. Because the leading 48 bits are a readable millisecond timestamp, anyone holding a UUIDv7 can call uuid_extract_timestamp() (or decode it client-side) to learn approximately when the row was created — and, across several IDs, infer your creation rate.2 For many internal tables that is harmless or even useful; for a public-facing identifier it can expose business signals like sign-up timing or order volume.

The common mitigation is to keep UUIDv7 as the internal primary key and expose a separate, fully random UUIDv4 (gen_random_uuid()) as the public-facing identifier, so external parties never see the time-ordered value.5 If even an internal timestamp is sensitive, the shift argument lets you offset the embedded time by a constant to obscure the true creation date — at the cost of the timestamp no longer being meaningful.1

Bottom line and next steps

PostgreSQL 18's native uuidv7() finally makes time-ordered UUID primary keys a one-line default with no extensions, giving you UUID's distributed-friendly keyspace with index behavior close to a bigint.12 Reach for it when you need globally unique or client-generated IDs; stick with bigint identity when a single-node app just needs the smallest key; and isolate the timestamp leak behind a separate public ID when creation time is sensitive.

Because UUIDv7 keys are naturally ordered, they pair perfectly with keyset (cursor) pagination in Postgres, which depends on a sortable key. If you are moving to PostgreSQL 18 to get uuidv7(), see the zero-downtime Postgres 18 upgrade guide, and once you are there, pg_partman + pg_cron partition automation on Postgres 18 works hand-in-hand with time-ordered keys for large tables.

Footnotes

  1. PostgreSQL 18 Documentation — 9.14. UUID Functions. https://www.postgresql.org/docs/18/functions-uuid.html 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

  2. Josef Machytka, credativ — "A deeper look at old UUIDv4 vs new UUIDv7 in PostgreSQL 18" (2025-12-05). https://www.credativ.de/en/blog/postgresql-en/a-deeper-look-at-old-uuidv4-vs-new-uuidv7-in-postgresql-18/ 2 3 4 5 6 7 8 9 10 11 12

  3. PostgreSQL Global Development Group — "PostgreSQL 18.4, 17.10, 16.14, 15.18, and 14.23 Released!" (2026-05-14). https://www.postgresql.org/about/news/postgresql-184-1710-1614-1518-and-1423-released-3297/

  4. Masahiko Sawada (PostgreSQL committer) — "PostgreSQL 18 supports UUIDv7" (2025-09-04). https://masahikosawada.github.io/en/2025/09/04/UUIDv7-in-PostgreSQL/

  5. CYBERTEC — "UUID, serial or identity columns for PostgreSQL auto-generated primary keys?" https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/ 2 3 4 5

Frequently Asked Questions

Yes. uuidv7() is a core PostgreSQL 18 function — no extension needed. It generates an RFC 9562 version 7, time-ordered UUID. 1