Generate a UUID in PostgreSQL

SELECT gen_random_uuid();

SELECT gen_random_uuid(); returns a random version 4 UUID. It has been built in since PostgreSQL 13; before that it came from the pgcrypto extension.

SELECT gen_random_uuid();   -- 86d9d9c9-8e80-4136-b903-548ba8654282

That ran on PostgreSQL 18.6. Fed those 32 hex digits as its entropy, the tool’s core returns the same UUID; the tool below draws fresh bytes in your browser instead.

The result has the uuid type, which takes 16 bytes on disk. The same value in a text column took 37 (by pg_column_size), and its index grows to match, so keep the uuid type.

Version 7 in PostgreSQL 18

PostgreSQL 18 added uuidv7(), and uuidv4() as another name for gen_random_uuid(). PostgreSQL 17 had already added functions that read a UUID’s version and time:

SELECT uuidv7();                    -- 01a13cd6-b946-7ca4-8f26-f99833f5850e
SELECT uuid_extract_version(u), uuid_extract_timestamp(u)
FROM (SELECT uuidv7() AS u) AS s;   -- 7 | 2026-10-14 23:54:27.527+00
SELECT uuid_extract_timestamp(gen_random_uuid()) IS NULL;   -- t
SELECT uuidv7(interval '-1 day');   -- 01a137b0-5d7b-7a9c-ab37-295776364500

The tool’s decoder reads the first one as made at 2026-10-14T23:54:27.526Z, and the last a day earlier: uuidv7(interval) shifts the timestamp it writes. PostgreSQL fills the 12 bits after the version digit with a sub-millisecond fraction of the clock (RFC 9562 section 6.2, method 3), so one session’s UUIDs keep increasing even within a millisecond. As a column default:

CREATE TABLE orders (id uuid PRIMARY KEY DEFAULT uuidv7(), total numeric);
INSERT INTO orders (total) VALUES (9.99), (24.50) RETURNING id;
-- 01a13cd6-b979-7916-9240-173dc05556f1
-- 01a13cd6-b97a-77e9-8c4d-cec2246d7e84

Why v7 helps B-tree indexes

A random key lands on a random leaf page of the primary key’s index, so pages split in the middle and stay partly empty, and every insert touches a different page. Each version 7 key is larger than the last, so inserts append at the right edge and pages fill to the index’s fillfactor. A million rows into each of two tables:

CREATE TABLE t4 (id uuid PRIMARY KEY DEFAULT uuidv4(), n int);
CREATE TABLE t7 (id uuid PRIMARY KEY DEFAULT uuidv7(), n int);
INSERT INTO t4 (n) SELECT g FROM generate_series(1, 1000000) g;
INSERT INTO t7 (n) SELECT g FROM generate_series(1, 1000000) g;
-- pgstatindex() comes from the pgstattuple extension
  index  | size  | leaf_pages | avg_leaf_density
---------+-------+------------+------------------
 t4_pkey | 38 MB |       4825 |            71.53
 t7_pkey | 30 MB |       3832 |            89.98

Both inserts took about 7 seconds, because both indexes fit in memory. The gap widens once an index doesn’t: then a version 4 insert may have to read its random page from disk first.

Gotchas

  • A version 7 key tells anyone who holds it when the row was created, through uuid_extract_timestamp or any decoder. Use version 4 for IDs that mustn’t show that.
  • On PostgreSQL 12 and older, CREATE EXTENSION pgcrypto; provides gen_random_uuid(), and the uuid-ossp extension provides uuid_generate_v4().
  • Casting text to uuid checks it: SELECT 'not-a-uuid'::uuid; fails with invalid input syntax for type uuid: "not-a-uuid".

Try it in the UUID Generator & Decoder

Drawing your browser’s random bytes, 16 per UUID…

Runs in your browser. Nothing you enter is sent anywhere.