Chapter 04 · Data Types, Domains, Constraints, Identity, and Generated Data

Identity Columns, Sequences, Sequence Caching, Gaps, and Distributed-ID Considerations

Understand identity columns and sequence objects as concurrency-safe identifier allocators, not gapless counters; observe session semantics, caching, rollback gaps, ownership, and privilege requirements.

Intermediate110–140 minutesIdentity + sequence semantics labCurrent patched PostgreSQL 18.xSequence gaps demonstrated safelyLast reviewed: August 2026

Learning outcomes

Identifiers need to be unique under concurrency, but “unique” is not the same requirement as “consecutive.” PostgreSQL sequence objects deliberately optimize for fast multi-session allocation. Values can be skipped because of rollback, conflicts, caching, crashes, or administrative changes. If an invoice number must be legally gapless, an identity/sequence column is not that ledger requirement.

This lesson makes identity and sequence semantics visible using separate transactions and sessions. You will inspect the implicit sequence behind an identity column, compare GENERATED ALWAYS with BY DEFAULT, use nextval/currval/setval safely, and compare sequence keys with UUID identifiers.

01

Explain identity columns as table columns backed by implicit sequence generators.

02

Predict nextval, currval, and setval session behavior.

03

Demonstrate why sequence allocation is not rolled back and therefore can contain gaps.

04

Explain cache tradeoffs, ownership associations, and sequence privileges.

05

Choose between sequence-backed integer IDs and UUID-style IDs from system boundaries rather than fashion.

1. Identity columns: SQL-facing intent, sequence-backed mechanism

GENERATED ... AS IDENTITY declares that PostgreSQL should generate values for a column through an implicit sequence. ALWAYS rejects ordinary explicit values unless the statement deliberately uses OVERRIDING SYSTEM VALUE; BY DEFAULT allows an explicit caller value to override generation.

sql · ALWAYS versus BY DEFAULT
CREATE TABLE app.ch04_identity_always (    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    note text NOT NULL);CREATE TABLE app.ch04_identity_default (    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,    note text NOT NULL);INSERT INTO app.ch04_identity_always(note) VALUES ('generated') RETURNING id;INSERT INTO app.ch04_identity_default(note) VALUES ('generated') RETURNING id;-- Ordinary explicit value should fail for ALWAYS:INSERT INTO app.ch04_identity_always(id,note) VALUES (100,'explicit');-- BY DEFAULT accepts an explicit value:INSERT INTO app.ch04_identity_default(id,note) VALUES (100,'explicit');

ALWAYS is useful when callers should not choose identifiers accidentally. BY DEFAULT can simplify imports/migrations that preserve source IDs, but then you must reconcile the sequence state before relying on generated values again.

2. Find the sequence instead of guessing its name

Identity implementation details should be inspected through supported interfaces. pg_get_serial_sequence() also works for identity columns despite its historical name.

sql · introspect identity metadata
SELECT column_name, is_identity, identity_generation,       identity_start, identity_incrementFROM information_schema.columnsWHERE table_schema='app'  AND table_name IN ('ch04_identity_always','ch04_identity_default')ORDER BY table_name, ordinal_position;SELECT pg_get_serial_sequence('app.ch04_identity_always','id') AS sequence_name;SELECT schemaname, sequencename, sequenceowner,       start_value, min_value, max_value, increment_by, cache_size, last_valueFROM pg_catalog.pg_sequencesWHERE schemaname='app' AND sequencename LIKE 'ch04_identity_%';

3. Standalone sequences and session semantics

A sequence is an independent database object. nextval() atomically advances it and returns a distinct value under concurrency. currval() returns the value most recently obtained from that sequence in the current session and errors if this session has never called nextval(). lastval() refers to whichever sequence this session used most recently.

sql · session-local currval
CREATE SEQUENCE app.ch04_ticket_seq START WITH 1000 INCREMENT BY 1 CACHE 1;SELECT nextval('app.ch04_ticket_seq') AS allocated;SELECT currval('app.ch04_ticket_seq') AS this_sessions_last_value;SELECT lastval() AS this_sessions_last_sequence_value;

Open a second psql session and call currval('app.ch04_ticket_seq') before nextval(). The expected error is evidence that currval is session-local, not a global “show me the current number” operation.

4. The rollback gap: allocation is intentionally non-transactional

PostgreSQL does not reclaim a value from nextval() when the transaction rolls back. This prevents allocators from blocking each other in order to recycle numbers and is fundamental to efficient concurrency.

sql · prove a rollback gap
BEGIN;SELECT nextval('app.ch04_ticket_seq') AS allocated_then_aborted;ROLLBACK;SELECT nextval('app.ch04_ticket_seq') AS next_after_rollback;

The second value advances past the aborted one. Similar holes can appear when INSERT ... ON CONFLICT computes a default sequence value before choosing the conflict path. Therefore sequence values are unique allocators, not evidence that N committed rows/events occurred.

Business rule distinction

If a regulator requires gapless issued document numbers, model that as a serialized issuance ledger with explicit transactional/locking semantics. Do not promise that an identity primary key will be gapless.

5. setval and ALTER SEQUENCE RESTART are not the same operation

setval() changes sequence state immediately and the change is not rolled back. The optional Boolean controls whether the next nextval returns exactly the specified value or advances first. In contrast, ALTER SEQUENCE ... RESTART is transactional and blocks concurrent sequence operations while it changes the restart position.

sql · safe disposable setval semantics
CREATE SEQUENCE app.ch04_setval_probe START 1 CACHE 1;SELECT setval('app.ch04_setval_probe', 42, false);SELECT nextval('app.ch04_setval_probe');  -- 42SELECT nextval('app.ch04_setval_probe');  -- 43BEGIN;ALTER SEQUENCE app.ch04_setval_probe RESTART WITH 100;ROLLBACK;SELECT nextval('app.ch04_setval_probe');

Never run sequence-reset commands against a valuable production sequence because a tutorial says “sync it.” First identify duplicates/collision risk, active writers, ownership, and the highest committed ID. Sequence repair belongs in a migration/runbook with verification.

6. Sequence caching: throughput versus visible gaps/order

CACHE n allows a backend to reserve multiple sequence values at once. This can reduce sequence contention, but multiple sessions may hold separate chunks. Values observed by commit time may appear out of numerical order, and unused cached values can become gaps when sessions exit.

sql · inspect and change cache on a disposable sequence
ALTER SEQUENCE app.ch04_ticket_seq CACHE 10;SELECT sequencename, cache_size, last_valueFROM pg_catalog.pg_sequencesWHERE schemaname='app' AND sequencename='ch04_ticket_seq';

Do not pick a universal cache value. Benchmark the real concurrency pattern, understand failover/replication and external-ID expectations, and treat the sequence number as an identifier—not a reliable event-order clock.

7. Ownership associations and privileges

A sequence can be associated with a table column via OWNED BY, which ties lifecycle: dropping the owned column/table can drop the sequence. Identity columns manage this relationship automatically. Standalone sequences default to no such ownership association.

Calling standalone sequence functions also requires privileges. nextval requires USAGE or UPDATE; currval requires USAGE or SELECT; setval requires UPDATE. For explicit sequence-backed defaults (including legacy SERIAL-style designs), table privileges do not automatically imply sequence privileges. Identity columns package generation as part of the column definition, so test the exact runtime role against the identity/table design instead of blindly copying legacy serial grants.

sql · inspect sequence privileges
SELECT has_sequence_privilege('servicehub_app','app.ch04_ticket_seq','USAGE') AS app_usage,       has_sequence_privilege('servicehub_app','app.ch04_ticket_seq','SELECT') AS app_select,       has_sequence_privilege('servicehub_app','app.ch04_ticket_seq','UPDATE') AS app_update;

Grant only what the runtime workflow needs. Do not grant UPDATE merely to make an error disappear if the app only needs nextval.

8. Identity/sequence integer versus UUID

Question Sequence-backed integer UUID
Central allocator needed? Database sequence allocates Can be generated independently
Width Typically 8 bytes for bigint 16 bytes
Human readability Shorter Long
Global/distributed uniqueness Scoped to allocator/design Designed for extremely low collision probability
Natural ordering Allocation order only, not commit order Version-dependent; UUIDv7 is time-ordered
Secrecy Not secret Not secret

ServiceHub can reasonably use an internal bigint primary key plus a public UUID when both compact joins and externally safe identifier generation are useful. Avoid duplicating identifiers without an architectural reason.

9. Allocation order is not commit order

Sequence values are assigned when nextval() runs, not when a transaction commits. Session A can receive 2001 and then perform slow work while Session B receives 2002 and commits immediately. A report ordered by the identifier may therefore look like an event timeline even when it is not the transaction-commit timeline. If business ordering matters, store an explicit timestamp or domain sequence whose semantics are designed for that ordering requirement.

text · two-session allocation timeline
Session A: BEGIN -> nextval = 2001 -> long work -----------------> COMMITSession B:        BEGIN -> nextval = 2002 -> COMMITObserved commit order: 2002, then 2001Observed allocation order: 2001, then 2002

This distinction becomes even more important with cached values and distributed application workers. Treat an identity as a key unless you have separately proved that its allocation properties satisfy the ordering contract.

10. Identity versus SERIAL: prefer the clearer modern declaration

PostgreSQL still supports the historical smallserial, serial, and bigserial pseudo-types. They expand into an integer column, a sequence, a nextval default, and ownership linkage. Identity columns express the generation relationship as a first-class column property in SQL metadata and support ALWAYS/BY DEFAULT override semantics. For new schemas, identity usually makes the intent easier for migrations and introspection to understand.

Existing production schemas using SERIAL are not automatically wrong and do not need cosmetic rewrites. Migration risk, sequence names, defaults, grants, dependencies, ORM assumptions, and replication/backup tooling matter more than syntax fashion. Teach the mechanism so you can operate both forms safely.

sql · metadata distinguishes default from identity
SELECT table_name, column_name, column_default, is_identity, identity_generationFROM information_schema.columnsWHERE table_schema='app'  AND table_name IN ('ch04_identity_always','ch04_identity_default')ORDER BY table_name, ordinal_position;

11. Deliberately wrong approach: SELECT max(id)+1

Two concurrent transactions can both read the same maximum and compute the same next value. Adding a unique constraint merely converts the race into errors. Sequences exist specifically to provide atomic multi-session allocation.

sql · anti-pattern to recognize, not deploy
-- Race-prone pseudo-code:SELECT COALESCE(max(id),0)+1 FROM app.some_table;-- Another session can compute the same value before either INSERT commits.

12. Hands-on lab: allocation timeline

  1. In Session A create/use app.ch04_ticket_seq and record nextval/currval.
  2. In Session B call currval before nextval and record the error.
  3. In Session A allocate inside a transaction and roll it back; prove the value is not reused.
  4. Change CACHE on the disposable sequence and inspect pg_sequences.
  5. Compare GENERATED ALWAYS and BY DEFAULT with explicit inserts.
  6. Write a design decision: which ServiceHub identifiers are internal sequence-backed keys and which, if any, need UUIDs?

Check your understanding

  1. Why can a sequence contain gaps after rollback?
  2. What is session-local about currval?
  3. How does ALWAYS identity differ from BY DEFAULT?
  4. What tradeoff does sequence CACHE introduce?
  5. Why is max(id)+1 unsafe under concurrency?
Review the answers

nextval allocation is not rolled back, so aborted values are not reused. currval reports the latest value obtained by the current session for that sequence. ALWAYS blocks ordinary explicit values; BY DEFAULT permits them. Caching reserves chunks for throughput and can make gaps/out-of-order observation more visible. max(id)+1 is a read-then-write race that multiple sessions can compute simultaneously.

13. Cleanup

sql · remove only identity/sequence probes
DROP TABLE IF EXISTS app.ch04_identity_always;DROP TABLE IF EXISTS app.ch04_identity_default;DROP SEQUENCE IF EXISTS app.ch04_ticket_seq;DROP SEQUENCE IF EXISTS app.ch04_setval_probe;

14. Summary and bridge

Identity columns make sequence-backed generation part of the table definition; sequence functions expose the allocator directly. PostgreSQL guarantees safe distinct allocation, not gaplessness, commit ordering, or secrecy. Next, generated columns and defaults will show a different kind of generation—computed data whose value depends on row expressions—then connect it to encoding and collation choices that can change comparison/index semantics across upgrades.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.