22 Identity Columns and Sequences
You rarely want to hand-pick primary key values. PostgreSQL gives you two ways to generate them automatically: the modern, SQL-standard GENERATED ... AS IDENTITY syntax, and the older serial / bigserial shortcuts. Both are powered by a sequence — an independent object that hands out incrementing numbers. IDENTITY is the recommended choice for new code; serial is still common in older codebases.
What you’ll learn
- The
GENERATED ALWAYS AS IDENTITYsyntax - The differences from the older
serialshortcut - What a sequence is and how to inspect it
- Why generated IDs can have gaps
- Resetting a sequence after a bulk import
Example
GENERATED ALWAYS AS IDENTITY tells PostgreSQL the column gets its value from the underlying sequence and cannot be overridden by INSERT (unless you use OVERRIDING SYSTEM VALUE). GENERATED BY DEFAULT AS IDENTITY is the more permissive variant — if the INSERT provides a value, it is used; otherwise the sequence supplies one. Use ALWAYS when you want to prevent accidental ID injection from application code; use BY DEFAULT when you want flexibility for migrations and tests.
The older serial (integer + sequence) and bigserial (bigint + sequence) are syntactic sugar for integer NOT NULL DEFAULT nextval('seq'). They still work, but IDENTITY is the SQL standard, plays better with pg_dump, and tracks ownership of the sequence more cleanly.
Sequences are independent of transactions: nextval() does not roll back. That means if a transaction inserts a row and rolls back, the ID is burned — the next row gets the value after it. IDs can have gaps, and your application should never assume they are contiguous.
Run the example
To run:
Common pitfalls
- Reading “the last ID I just inserted” using
currval()from a different connection — it won’t work. Sequences are shared objects; only the value remembered bycurrval()is per-session. UseRETURNING idinstead. - After a bulk import that supplied explicit IDs, the sequence is still at its old value, so the next auto-generated ID collides. Reset it:
SELECT setval('t_id_seq', (SELECT max(id) FROM t));. - Counting on IDs to be sequential or to reflect insertion order — they don’t, especially under concurrency. Sort by
created_at, idif you need order. - Using
integer(32-bit) for an IDENTITY column on a high-volume table — you can exhaust the range. Usebigint.
When migrating from MySQL’s AUTO_INCREMENT, the safest direct equivalent is bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY. It accepts explicit IDs (good for migrations) and otherwise auto-generates.
Run two INSERTs into messages_example, then ROLLBACK one of them inside a transaction (BEGIN; INSERT ...; ROLLBACK;), then INSERT again — you should see a gap in the IDs. Then reset a sequence with setval() and observe the next INSERT resume from there.
Downloads
SQL file: identity-and-sequences.sql · All lesson files · Setup guide