Auto-Generate UUIDs in PostgreSQL: gen_random_uuid(), uuidv7() and DEFAULT
PostgreSQL can generate UUIDs itself, so the application does not have to: give the column a default and every `INSERT` that omits the ID gets a new one. Which function to use depends on your version. `gen_random_uuid()` has been built in since PostgreSQL 13, and PostgreSQL 18 added `uuidv7()`, which produces time-ordered values that are kinder to indexes. Every statement below was run on PostgreSQL 18.6, and the version-specific ones were checked on PostgreSQL 13 as well.
The short answer: a DEFAULT on a uuid column
Use the native uuid type, not text, and set the default when you create the table:
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
total numeric NOT NULL
);
INSERT INTO orders (total) VALUES (10), (20) RETURNING id;
-- id
-- --------------------------------------
-- e56d515b-7d37-47a3-9911-1f8e12895e79
-- 6988f789-c1be-4a5d-93fc-86ad5a2468b9
RETURNING id hands the generated value back to the application in the same round trip, which is how ORMs and query builders read it. The default applies only when the column is omitted or written as DEFAULT; an explicit value in the INSERT is stored as given, which is useful for imports and for IDs generated on the client.
The uuid type stores 16 bytes, while the same value as text takes 40 bytes in our test (pg_column_size). It also validates input, compares faster and indexes smaller. There is no reason to store UUIDs as varchar(36) in PostgreSQL.
gen_random_uuid(): random version 4
gen_random_uuid() returns a version 4 UUID with 122 random bits from a cryptographically strong source. It became a core function in PostgreSQL 13; in versions 9.4 to 12 it was available only after CREATE EXTENSION pgcrypto. PostgreSQL 18 also adds uuidv4() as a clearer alias:
SELECT uuid_extract_version(gen_random_uuid()), -- 4
uuid_extract_version(uuidv4()); -- 4 (PostgreSQL 18+)
Version 4 is the safe default when IDs appear in URLs or APIs, because it reveals nothing about when or where a row was created. Its weakness is physical: random keys land in random places in the primary-key B-tree, so on large, insert-heavy tables they cause more page splits and more of the index has to stay in memory than with sequential keys.
uuidv7(): time-ordered UUIDs in PostgreSQL 18
Version 7 UUIDs, standardised in RFC 9562 in 2024, start with a 48-bit Unix timestamp in milliseconds, followed by random bits. Values generated later sort later, so new rows go to the right-hand end of the index, like a sequence, while IDs stay globally unique without coordination. PostgreSQL 18 generates them natively:
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(),
name text
);
INSERT INTO events (name) VALUES ('a'), ('b')
RETURNING id, uuid_extract_version(id), uuid_extract_timestamp(id);
-- 01a154f9-bd4a-7dd3-b58c-e652a0cdb464 | 7 | 2026-10-19 16:23:35.498+00
-- 01a154f9-bd4b-7030-9330-adf3f754a2cb | 7 | 2026-10-19 16:23:35.499+00
PostgreSQL's implementation also fills the bits after the millisecond timestamp with a sub-millisecond fraction, so ordering is finer than one millisecond. The optional argument shifts the embedded timestamp, for example uuidv7(interval '-1 hour'), which is handy for backfilling. On PostgreSQL 17 and earlier, uuidv7() does not exist (we got function uuidv7() does not exist on 13), so generate v7 values in the application or with an extension until you upgrade.
The trade-off: anyone who sees a v7 ID can read the creation time to the millisecond with uuid_extract_timestamp(). For internal keys that is usually fine; for public identifiers where timing is sensitive, keep v4.
Adding a UUID column to an existing table
Adding a column with a volatile default such as gen_random_uuid() fills every existing row with its own value, because PostgreSQL rewrites the table:
ALTER TABLE customers ADD COLUMN public_id uuid NOT NULL DEFAULT gen_random_uuid();
-- every existing row gets a distinct UUID (3 rows -> 3 distinct values in our test)
-- two-step variant: the default applies only to future rows
ALTER TABLE customers ADD COLUMN ref uuid;
ALTER TABLE customers ALTER COLUMN ref SET DEFAULT gen_random_uuid();
UPDATE customers SET ref = gen_random_uuid() WHERE ref IS NULL;
The one-statement form takes an ACCESS EXCLUSIVE lock for the duration of the rewrite, which blocks reads and writes on a large table. On busy production tables, use the two-step version and run the UPDATE in batches, then add NOT NULL and a unique index (CREATE UNIQUE INDEX CONCURRENTLY) once every row is filled.
Is uuid-ossp still needed?
Only for UUID versions other than 4 and 7. The uuid-ossp extension provides uuid_generate_v1(), v1mc(), v3(), v4() and v5(). Many tutorials still begin with CREATE EXTENSION "uuid-ossp" and DEFAULT uuid_generate_v4(), which works but is unnecessary on PostgreSQL 13 and later, where gen_random_uuid() needs no extension at all.
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v5(uuid_ns_url(), 'https://example.com/products/42');
-- 34adb0ec-5647-5afd-9b6d-76be74ef5892 — the same name always gives the same UUID
Version 5 is the real reason to install it today: it derives a stable UUID from a namespace and a name, so the same input always yields the same ID across databases. Avoid version 1 for new designs, because it embeds a timestamp and a node identifier, often derived from a MAC address, in a format that does not sort usefully.
Choosing between v4, v7 and bigint
If your PostgreSQL is 18 or newer and IDs are not exposed where creation time is sensitive, uuidv7() is the best default for primary keys: unique without coordination, safe to generate on clients too, and index-friendly. Use gen_random_uuid() when IDs are public and should reveal nothing, or on versions 13 to 17. A bigint GENERATED ALWAYS AS IDENTITY key is still smaller (8 bytes) and fastest, and it remains a good choice for purely internal tables in a single database.
Whatever you choose, generate test values with the same version as production. A UUID generator produces v4 and v7 values for fixtures, and the UUID v7 length article explains the format of the timestamp bits.
Sources and further reading
The PostgreSQL 18 documentation, "UUID Functions" page, describes gen_random_uuid(), uuidv4(), uuidv7(), uuid_extract_timestamp() and uuid_extract_version(); the PostgreSQL 13 release notes record gen_random_uuid() moving into core. The uuid-ossp and pgcrypto module documentation covers the extension functions. RFC 9562 defines UUID versions 4, 5 and 7. The ALTER TABLE reference explains when adding a column rewrites the table.
Frequently asked questions
How do I auto-generate a UUID in PostgreSQL?
Declare the column as uuid with DEFAULT gen_random_uuid(), for example id uuid PRIMARY KEY DEFAULT gen_random_uuid(). Inserts that omit the column get a new random UUID, and RETURNING id gives it back to the application.
Does PostgreSQL have a built-in UUID function?
Yes. gen_random_uuid() is built in since PostgreSQL 13, and PostgreSQL 18 adds uuidv4() and uuidv7(). Before version 13, gen_random_uuid() required the pgcrypto extension.
How do I generate UUID v7 in PostgreSQL?
On PostgreSQL 18 or newer, call uuidv7() or use it as a column default. Older versions do not have it, so generate v7 values in the application or with an extension.
Should I use gen_random_uuid() or uuid_generate_v4()?
Use gen_random_uuid(). Both return version 4 UUIDs, but uuid_generate_v4() needs the uuid-ossp extension, while gen_random_uuid() is part of core PostgreSQL since version 13.
How do I add a UUID column to an existing table?
ALTER TABLE t ADD COLUMN id uuid NOT NULL DEFAULT gen_random_uuid() fills every row with its own UUID, but rewrites the table under a lock. For big tables, add the column, set the default, and backfill in batches.
Are UUID primary keys slow in PostgreSQL?
Random v4 keys cause more index page splits and cache misses than sequential keys on large, write-heavy tables. Time-ordered uuidv7() keys avoid most of that cost while staying globally unique.