SERIAL & Identity Columns
Most tables need a column that auto-generates a unique, incrementing number for each new row — the classic surrogate primary key. PostgreSQL offers two ways to do this: the long-standing SERIAL / BIGSERIAL shorthand, and the newer, SQL-standard GENERATED ... AS IDENTITY syntax.
SERIAL: the classic shorthand
SERIAL isn't really a distinct type — it's shorthand that PostgreSQL expands, at table-creation time, into an INTEGER column, a backing sequence object to generate the next value, and a DEFAULT that pulls from that sequence. BIGSERIAL does the same thing on top of BIGINT for when you expect to exceed the INTEGER range.
SERIAL primary key
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO customers (name) VALUES ('Ada Lovelace'), ('Alan Turing');
SELECT * FROM customers;customer_id | name
------------+-----------------
1 | Ada Lovelace
2 | Alan TuringGENERATED ... AS IDENTITY: the modern alternative
GENERATED ALWAYS AS IDENTITY (and its sibling GENERATED BY DEFAULT AS IDENTITY) is the SQL-standard way to express the same idea, added to PostgreSQL in version 10. It behaves like SERIAL day-to-day, but with clearer, more portable semantics.
Identity column primary key
CREATE TABLE orders (
order_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_date DATE NOT NULL DEFAULT CURRENT_DATE
);
INSERT INTO orders (order_date) VALUES (DEFAULT);
-- GENERATED ALWAYS rejects an explicit value unless you say OVERRIDING SYSTEM VALUE:
INSERT INTO orders (order_id, order_date) VALUES (999, '2026-01-01');
-- error: cannot insert a non-DEFAULT value into column "order_id"
INSERT INTO orders (order_id, order_date) OVERRIDING SYSTEM VALUE
VALUES (999, '2026-01-01'); -- explicitly allowedSERIAL vs IDENTITY
SERIAL / BIGSERIAL | GENERATED ... AS IDENTITY | |
|---|---|---|
Standard SQL | No — PostgreSQL-specific | Yes |
Explicit insert of a value | Always silently allowed (since it's just a default) |
|
Underlying mechanism | A real, separately-owned sequence plus a | A sequence tightly bound to the column, cleaned up automatically with it |
Recommended for new code | Legacy default, still very common | Yes — PostgreSQL's own docs recommend this for new schemas |
SERIAL/BIGSERIALare shorthand for an integer column backed by a sequence — not a true type of their own.GENERATED ALWAYS AS IDENTITYis the SQL-standard, PostgreSQL-recommended alternative for new tables.GENERATED ALWAYSrejects explicit inserts into the identity column unless you useOVERRIDING SYSTEM VALUE.Both ultimately rely on a sequence — see the Sequences page for how that mechanism works.