Course topics

By WebNest Studio Engineering Team

PostgreSQL Tutorial

Sequences, SERIAL, and GENERATED IDENTITY Columns

Most tables need a column that auto-generates a unique, incrementing number for each new row — a surrogate primary key. PostgreSQL implements this using sequence objects: standalone counters that hand out the next number on demand, independent of any table.

Over the years PostgreSQL has offered two ways to attach a sequence to a column: the older SERIAL pseudo-type, and the SQL-standard GENERATED AS IDENTITY syntax introduced in PostgreSQL 10. Modern code should prefer identity columns, but you will see SERIAL constantly in existing codebases and tutorials.

Why it matters

Almost every table needs an id that the database assigns by itself. PostgreSQL has an older way of doing this, SERIAL, and a newer standard one, identity columns. Existing schemas are full of the first and new ones should use the second, so you need to recognise both and know how they differ.

SERIAL and BIGSERIAL (Legacy)

SERIAL is shorthand that PostgreSQL expands, behind the scenes, into an INTEGER column, a backing sequence (e.g. tablename_columnname_seq), and a DEFAULT nextval('tablename_columnname_seq'). BIGSERIAL does the same with BIGINT. It works, but it is not a true type — it is a convenience macro, and because the sequence is a separate object, ownership and dependency management can get confusing (for example, dumping/restoring or copying schemas can lose the link between column and sequence if done manually).

GENERATED ... AS IDENTITY (Modern)

The SQL-standard approach declares intent explicitly: id INTEGER GENERATED ALWAYS AS IDENTITY or GENERATED BY DEFAULT AS IDENTITY. ALWAYS means the database controls the value and rejects an explicit INSERT into that column unless you use OVERRIDING SYSTEM VALUE; BY DEFAULT allows an explicit value to be provided, falling back to the sequence otherwise. Identity columns are the recommended modern approach because they are properly tracked as part of the column's definition by PostgreSQL's catalog and tooling, rather than being an implicit side effect.

Working with the Underlying Sequence

Whichever syntax you use, you can inspect and control the sequence directly: currval() returns the last value obtained in the current session, nextval() advances and returns the next value, and setval() resets the counter (useful after bulk-loading data with explicit IDs). Sequences are also useful standalone, unattached to any column, whenever you need a globally increasing counter.

Example

SERIAL vs. the modern GENERATED ALWAYS AS IDENTITY syntax

SQL
-- Legacy style
CREATE TABLE legacy_customers (
    id   SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);

-- Modern, SQL-standard style
CREATE TABLE customers (
    id   INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL
);

INSERT INTO customers (name) VALUES ('Meera Nair'), ('Aditya Rao');
SELECT * FROM customers;
Output
CREATE TABLE
CREATE TABLE
INSERT 0 2
 id |    name
----+-------------
  1 | Meera Nair
  2 | Aditya Rao

Common Mistakes

  • Trying to INSERT an explicit value into a GENERATED ALWAYS AS IDENTITY column without OVERRIDING SYSTEM VALUE, and being surprised by the resulting error.
  • Forgetting to bump the sequence with setval() after bulk-loading rows with explicit IDs, causing the next auto-generated INSERT to collide with an existing primary key.
  • Assuming SERIAL creates a true data type — it is INTEGER (or BIGINT for BIGSERIAL) plus a sequence and default, which shows up as "integer" in \d output, not "serial".
  • Choosing SERIAL/INTEGER identity for a very high-write table expected to exceed about two billion rows, instead of BIGSERIAL/BIGINT, and hitting the range limit later.

Exercise

This exercise uses PostgreSQL identity columns, so it needs a PostgreSQL server. Create a customers table whose id is a BIGINT identity column that the database always generates, and which is the primary key. Insert two customers without giving an id, returning the id each time.

Expected result: the two inserts return 1 and 2. An insert that supplies its own id would be rejected.

SQL · starter code
-- TODO: customers (id generated always as identity, name)

-- TODO: insert 'Asha' and return the generated id

-- TODO: insert 'Ravi' and return the generated id
Show hints
  • The column is declared id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY.
  • The new value is returned with RETURNING id.
Show solution
SQL · solution
CREATE TABLE customers (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name TEXT NOT NULL
);

INSERT INTO customers (name) VALUES ('Asha') RETURNING id;

INSERT INTO customers (name) VALUES ('Ravi') RETURNING id;

Quick quiz

1. What is the difference between GENERATED ALWAYS and GENERATED BY DEFAULT?

  • A. There is none
  • B. BY DEFAULT generates random values
  • C. ALWAYS allows NULL
  • D. ALWAYS rejects a value supplied by the INSERT; BY DEFAULT uses the supplied value if there is one
Show answer

D. ALWAYS can still be overridden explicitly with OVERRIDING SYSTEM VALUE.

2. An insert takes id 5 from a sequence and its transaction is rolled back. What id does the next insert get?

  • A. 5 again
  • B. 6; sequence values are not returned on rollback, so gaps are normal
  • C. 1
  • D. An error
Show answer

B. Sequences guarantee uniqueness, not a gap-free series.

3. Why is BIGINT often preferred to INTEGER for an id column?

  • A. An INTEGER runs out at about 2.1 billion, and changing the type of a large table later is painful
  • B. It is faster to insert
  • C. It uses less space
  • D. INTEGER cannot be a primary key
Show answer

A. The extra four bytes per row are a small price for never hitting the limit.

Interview questions

What is the difference between SERIAL and an identity column?

Show answer

SERIAL is a PostgreSQL shorthand that creates an integer column, a separate sequence and a default that reads from it; the sequence is only loosely tied to the column, and its permissions are managed separately. GENERATED ... AS IDENTITY is the SQL-standard form, available since PostgreSQL 10: the sequence belongs to the column, and GENERATED ALWAYS prevents values being supplied by accident. Identity columns are recommended for new tables.

Can you rely on generated ids having no gaps?

Show answer

No. A sequence value is consumed when it is requested and is not given back if the transaction rolls back or the insert fails, and sequences may also cache values that are lost on a restart. Ids are therefore unique and increasing but not consecutive. A business requirement for gap-free numbers, as for invoices in some jurisdictions, needs a separate, deliberately serialised numbering scheme.

Key Points to Remember

  • SERIAL/BIGSERIAL are legacy shorthand for an integer column plus a backing sequence and default.
  • GENERATED ALWAYS/BY DEFAULT AS IDENTITY is the modern, SQL-standard way to auto-generate primary key values.
  • ALWAYS rejects explicit inserts unless overridden; BY DEFAULT allows them.
  • Sequences can be inspected and controlled directly with nextval(), currval(), and setval().

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering