Identity columns and UUIDs

PostgreSQL · Essentials · 10 min

Every table needs a key generator, and the modern options avoid serial's permission and ownership quirks.

The modern way to auto-generate integer keys is an identity column, standard SQL since PostgreSQL 10:

  • GENERATED ALWAYS AS IDENTITY rejects explicit values unless you write OVERRIDING SYSTEM VALUE — good protection against accidental manual ids.
  • GENERATED BY DEFAULT AS IDENTITY uses the sequence unless you supply a value — convenient for imports.

You'll still see the older serial / bigserial pseudo-types. They create a separate sequence plus a default, which causes quirks with permissions and copying table definitions; prefer identity in new schemas.

Both are backed by a sequence, which hands out numbers without waiting for transactions to finish. Consequence: a rolled-back insert still consumes its number, so ids have gaps. Never rely on ids being contiguous or use them as a count.

For UUIDs, use the uuid type (16 bytes, not text). gen_random_uuid() (built in since 13) gives random version-4 UUIDs. PostgreSQL 18 adds uuidv7(), which is time-ordered: new values sort after older ones, so B-tree inserts stay at the right edge of the index instead of landing at random pages. Prefer v7 for primary keys when you choose UUIDs.

After a bulk load with explicit ids, move the sequence past the maximum with setval, or the next insert will collide.

Example

CREATE TABLE customer (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name  text NOT NULL
);
CREATE TABLE api_key (
    id    uuid PRIMARY KEY DEFAULT gen_random_uuid(),   -- uuidv7() on 18+
    owner bigint NOT NULL REFERENCES customer(id)
);
INSERT INTO customer (name) VALUES ('Ani'), ('Davit');
INSERT INTO customer (id, name) VALUES (99, 'Manual');
-- ERROR: cannot insert a non-DEFAULT value into column "id"

BEGIN; INSERT INTO customer (name) VALUES ('Rolled back'); ROLLBACK;
INSERT INTO customer (name) VALUES ('Lilit');
SELECT id, name FROM customer ORDER BY id;   -- 1 Ani, 2 Davit, 4 Lilit (gap)
INSERT INTO api_key (owner) VALUES (1) RETURNING id;
-- e.g. 0b6f3c1e-8a7d-4c2e-9f51-3d2a6b7c8e90

Remember

  • Prefer GENERATED ... AS IDENTITY over serial
  • Sequences never roll back, so ids have gaps
  • Store UUIDs in the uuid type; uuidv7() keeps inserts index-friendly

Watch out for

  • Storing UUIDs as text, which doubles their size and slows comparisons.

Check yourself

  1. A transaction inserts a row into an identity table and then rolls back. What happens to the generated id?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.postgresql.org/docs/current/ddl-identity-columns.html