PostgreSQLSERIAL & Identity Columns

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

SQL
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 Turing
Note
Behind the scenes, `SERIAL` created a sequence named something like `customers_customer_id_seq` and wired its `nextval()` in as the column's default. The **Sequences** page covers that underlying mechanism — and how to control it directly — in more detail.
GENERATED ... 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

SQL
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 allowed
SERIAL 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)

GENERATED ALWAYS rejects it unless you opt in with OVERRIDING SYSTEM VALUE

Underlying mechanism

A real, separately-owned sequence plus a DEFAULT

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

Warning
A `SERIAL` column's underlying sequence is a separate database object with its own ownership and permissions, which occasionally causes confusion — dropping the column doesn't always clean up the sequence the way people expect, and privileges on the sequence have to be managed alongside privileges on the table. `IDENTITY` columns bind the sequence more tightly to the column and avoid this class of surprise.
Note
PostgreSQL's own documentation now recommends using `GENERATED ... AS IDENTITY` instead of `SERIAL` for new tables. It offers the same auto-increment behavior with standard, portable SQL syntax and cleaner semantics around explicit inserts — `SERIAL` remains extremely common in existing schemas and isn't going away, but for new code, `IDENTITY` is the better default.
  • SERIAL / BIGSERIAL are shorthand for an integer column backed by a sequence — not a true type of their own.

  • GENERATED ALWAYS AS IDENTITY is the SQL-standard, PostgreSQL-recommended alternative for new tables.

  • GENERATED ALWAYS rejects explicit inserts into the identity column unless you use OVERRIDING SYSTEM VALUE.

  • Both ultimately rely on a sequence — see the Sequences page for how that mechanism works.