SERIAL in PostgreSQL

Learn how the PostgreSQL SERIAL pseudo-type creates auto-incrementing integer columns, the three SERIAL variants, and when to prefer GENERATED AS IDENTITY instead.

4 min read · Back to overview

Quick Answer

SERIAL is a PostgreSQL pseudo-type that creates an integer column backed by an auto-incrementing sequence with a NOT NULL constraint. Use SMALLSERIAL (2-byte), SERIAL (4-byte), or BIGSERIAL (8-byte). For new tables, the SQL-standard GENERATED ALWAYS AS IDENTITY is preferred.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

PostgreSQL’s SERIAL pseudo-type provides a convenient shorthand for auto-incrementing integer columns. It is most commonly used for primary key columns where the database should assign unique IDs automatically. Under the hood, PostgreSQL creates a SEQUENCE object, sets the column default to nextval(), and adds NOT NULL — all in one step.

Syntax

CREATE TABLE table_name (
  id SERIAL [PRIMARY KEY],
  ...
);

This is exactly equivalent to:

CREATE SEQUENCE table_name_id_seq;
CREATE TABLE table_name (
  id INTEGER NOT NULL DEFAULT nextval('table_name_id_seq')
);
ALTER SEQUENCE table_name_id_seq OWNED BY table_name.id;

The three SERIAL variants:

TypeStorageRange
SMALLSERIAL2 bytes1 to 32,767
SERIAL4 bytes1 to 2,147,483,647
BIGSERIAL8 bytes1 to 9,223,372,036,854,775,807

Practical Example

Create a products table with a SERIAL primary key:

CREATE TABLE products (
  product_id  SERIAL PRIMARY KEY,
  sku         VARCHAR(50)    NOT NULL UNIQUE,
  name        VARCHAR(200)   NOT NULL,
  unit_price  NUMERIC(10,2)  NOT NULL DEFAULT 0
);

Insert rows by omitting the product_id column:

INSERT INTO products (sku, name, unit_price)
VALUES
  ('WID-001', 'Basic Widget',  9.99),
  ('WID-002', 'Premium Widget', 24.99);

Use RETURNING to capture the generated ID immediately:

INSERT INTO products (sku, name, unit_price)
VALUES ('WID-003', 'Deluxe Widget', 49.99)
RETURNING product_id;

 product_id
------------
          3
(1 row)

Check the current sequence value:

SELECT currval(pg_get_serial_sequence('products', 'product_id'));

Add a SERIAL primary key to an existing table:

ALTER TABLE categories ADD COLUMN category_id SERIAL PRIMARY KEY;

SERIAL vs GENERATED AS IDENTITY

For new tables, prefer the SQL-standard GENERATED ALWAYS AS IDENTITY (PostgreSQL 10+):

-- Modern approach
product_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY

-- Legacy shorthand
product_id SERIAL PRIMARY KEY

GENERATED ALWAYS AS IDENTITY prevents accidental overwrites — an insert that supplies an explicit value raises an error unless you use OVERRIDING SYSTEM VALUE.

Testing with Vela

When migrating a schema from SERIAL to GENERATED AS IDENTITY, or when resetting sequences after a bulk data load, Vela database branching lets you run and validate the migration on a full copy of production data before applying it to the live database. Test the sequence reset, verify no duplicate-key errors on subsequent inserts, then apply to production.

Production Tips

  • SERIAL does not automatically create a primary key — add PRIMARY KEY explicitly.
  • Prefer BIGSERIAL for tables expected to grow beyond a few hundred million rows to avoid sequence exhaustion.
  • Sequence values are not transaction-safe: rolled-back transactions permanently skip the consumed value, creating gaps. This is normal — never write business logic that assumes gap-free IDs.
  • After bulk data imports with explicit IDs, resync the sequence: SELECT setval(pg_get_serial_sequence('products', 'product_id'), MAX(product_id)) FROM products;
  • For new tables, use GENERATED ALWAYS AS IDENTITY instead of SERIAL — it is the SQL standard and safer against accidental manual inserts.

Continue in Managing Tables: Sequences.

Related in this section: PostgreSQL Data Types · Create Table · Select Into

Frequently Asked Questions

What does SERIAL do in PostgreSQL?
SERIAL is shorthand for creating an integer column with a linked auto-incrementing sequence and a NOT NULL constraint. When you omit the column from an INSERT, the sequence's next value is used automatically. SERIAL itself does not create a primary key — you must add PRIMARY KEY explicitly.
Does SERIAL lock the table and block queries during inserts?
No. Sequence value generation (nextval) is intentionally lock-free across sessions — each session gets a unique value without blocking other inserts. The table-level lock during an INSERT is much shorter than a sequence lock.
What happens to dependent objects when a SERIAL column is dropped?
When you drop a SERIAL column, PostgreSQL also drops the associated sequence because the sequence is owned by (OWNED BY) that column. Any default expression referencing the sequence is also removed.
Can I insert an explicit value into a SERIAL column?
Yes. You can supply an explicit integer in the INSERT. However, doing so does not advance the sequence, so a later INSERT without an explicit value might produce a duplicate-key error if the sequence reaches the same number. Use setval() to resync the sequence after bulk loads.
What is the safest way to use SERIAL in production?
Use BIGSERIAL (8-byte) rather than SERIAL (4-byte) for any table that could grow beyond a few hundred million rows. After bulk data imports, resync the sequence with SELECT setval(pg_get_serial_sequence('table','col'), MAX(col)) FROM table; to prevent duplicate-key errors.