Skip to main content

PostgreSQL Serial Types

Introduction

When designing database tables, you often need a column that automatically generates a unique identifier for each new row. In PostgreSQL, serial types provide an elegant solution for this common requirement.

Serial types create auto-incrementing columns that generate a new, unique integer value each time you insert a row. They're commonly used for primary keys and are essential to understand when working with PostgreSQL tables.

In this guide, we'll explore:

  • What serial types are and how they work
  • The different serial types available in PostgreSQL
  • How to create and use serial columns
  • Best practices and common pitfalls
  • Real-world applications

Understanding Serial Types

What Are Serial Types?

A serial type in PostgreSQL is a convenience notation that automatically:

  1. Creates a sequence (a special database object that generates sequential numbers)
  2. Sets the column's default value to pull from this sequence
  3. Applies NOT NULL constraint to the column

This means every time you insert a new row without specifying a value for this column, PostgreSQL will automatically assign the next value from the sequence.

Available Serial Types

PostgreSQL offers three serial types, each with different ranges:

TypeStorage SizeRangeEquivalent To
SMALLSERIAL2 bytes1 to 32,767SMALLINT NOT NULL DEFAULT nextval('sequence_name')
SERIAL4 bytes1 to 2,147,483,647INTEGER NOT NULL DEFAULT nextval('sequence_name')
BIGSERIAL8 bytes1 to 9,223,372,036,854,775,807BIGINT NOT NULL DEFAULT nextval('sequence_name')

Choose the appropriate type based on how many rows you expect your table to contain throughout its lifetime.

Creating Tables with Serial Columns

Basic Syntax

The basic syntax for creating a table with a serial column is:

sql
CREATE TABLE table_name (
id SERIAL PRIMARY KEY,
column_name data_type,
...
);

This creates a table with an auto-incrementing id column that will be used as the primary key.

Example: Creating a Simple Table

Let's create a products table with an auto-incrementing ID:

sql
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
description TEXT
);

Inserting Data

When inserting data, you can omit the serial column, and it will be automatically populated:

sql
INSERT INTO products (name, price, description)
VALUES ('Laptop', 999.99, 'High-performance laptop');

The result after running this command:

 product_id |  name  |  price  |        description        
------------+--------+---------+---------------------------
1 | Laptop | 999.99 | High-performance laptop

If you insert more rows:

sql
INSERT INTO products (name, price, description)
VALUES ('Phone', 499.99, 'Smartphone with 128GB');

INSERT INTO products (name, price, description)
VALUES ('Tablet', 349.99, '10-inch tablet');

The table will now look like:

 product_id |  name  |  price  |        description        
------------+--------+---------+---------------------------
1 | Laptop | 999.99 | High-performance laptop
2 | Phone | 499.99 | Smartphone with 128GB
3 | Tablet | 349.99 | 10-inch tablet

How Serial Works Behind the Scenes

When you create a serial column, PostgreSQL actually performs several operations:

  1. Creates a sequence object (named tablename_columnname_seq)
  2. Sets the column's default value to nextval('tablename_columnname_seq')
  3. Sets the column to NOT NULL

Let's see this in action. The following statements are equivalent:

sql
-- Using SERIAL (simplified version)
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100)
);

-- Expanded version (what PostgreSQL actually does)
CREATE SEQUENCE products_product_id_seq;

CREATE TABLE products (
product_id INTEGER NOT NULL DEFAULT nextval('products_product_id_seq'),
name VARCHAR(100),
PRIMARY KEY (product_id)
);

-- Gives ownership of the sequence to the product_id column
ALTER SEQUENCE products_product_id_seq OWNED BY products.product_id;

Understanding this behind-the-scenes behavior is crucial when you need to:

  • Reset sequences
  • Handle database migrations
  • Deal with data imports or restores

Managing Serial Columns

Finding the Current Sequence Value

To check the current value of a sequence:

sql
SELECT last_value FROM products_product_id_seq;

Resetting a Sequence

Sometimes you may need to reset a sequence, such as after bulk imports:

sql
-- Reset the sequence to start from 1
ALTER SEQUENCE products_product_id_seq RESTART WITH 1;

-- Or set it to start after the maximum existing ID
ALTER SEQUENCE products_product_id_seq RESTART WITH (SELECT MAX(product_id) + 1 FROM products);

Explicitly Setting Serial Values

While serials auto-increment by default, you can also explicitly set values:

sql
INSERT INTO products (product_id, name, price)
VALUES (100, 'Special Product', 1299.99);

Important note: When you explicitly set a serial column's value, PostgreSQL doesn't update the sequence automatically. This means subsequent inserts might cause conflicts if the sequence generates a value that already exists.

To avoid this, you should reset the sequence after explicit inserts:

sql
ALTER SEQUENCE products_product_id_seq RESTART WITH 101;

Real-World Applications

User Management System

A user management system typically uses serial for user IDs:

sql
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Adding a few users
INSERT INTO users (username, email)
VALUES
('john_doe', '[email protected]'),
('jane_smith', '[email protected]');

-- Creating a related table for user profiles
CREATE TABLE profiles (
profile_id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id),
full_name VARCHAR(100),
bio TEXT,
avatar_url VARCHAR(255)
);

-- Adding profiles for our users
INSERT INTO profiles (user_id, full_name, bio)
VALUES
(1, 'John Doe', 'Software developer from Seattle'),
(2, 'Jane Smith', 'UX Designer with 5 years experience');

E-commerce Order System

An e-commerce system might use BIGSERIAL for order numbers, anticipating millions of orders:

sql
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_amount DECIMAL(10, 2) NOT NULL
);

-- Creating order items table with its own serial
CREATE TABLE order_items (
item_id SERIAL PRIMARY KEY,
order_id BIGINT REFERENCES orders(order_id),
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL
);

-- Adding an order
INSERT INTO orders (customer_id, total_amount)
VALUES (1001, 1499.97);

-- Get the generated order_id
DO $$
DECLARE
new_order_id BIGINT;
BEGIN
SELECT last_value INTO new_order_id FROM orders_order_id_seq;

-- Add items to this order
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
(new_order_id, 1, 1, 999.99),
(new_order_id, 2, 1, 499.98);
END $$;

Serial vs. Identity Columns (PostgreSQL 10+)

In PostgreSQL 10 and later, the SQL standard IDENTITY columns were introduced as an alternative to SERIAL:

sql
CREATE TABLE modern_products (
product_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(100) NOT NULL
);

The difference between SERIAL and IDENTITY:

FeatureSERIALIDENTITY
SQL StandardNo (PostgreSQL specific)Yes
Control over value generationCan be overridden with explicit valuesMore strict - requires special syntax to override
Sequence ownershipSeparate object with linkIntegrated with column

For most new projects on PostgreSQL 10+, consider using IDENTITY columns instead of SERIAL for better standards compliance.

Common Pitfalls and Solutions

Pitfall 1: Sequence-Table Mismatch After Import/Export

When you export and import data, sequences often don't get updated properly.

Solution: After importing data, reset the sequence:

sql
-- Reset sequence to the maximum ID value plus one
SELECT setval('products_product_id_seq', (SELECT MAX(product_id) FROM products));

Pitfall 2: Gaps in Serial Values

Serial numbers can have gaps if:

  • Transactions are rolled back
  • Explicit values are inserted
  • Rows are deleted

Solution: If continuous values are critical (rare), don't use serials. Otherwise, accept gaps as normal.

Pitfall 3: Running Out of Values

For high-volume tables, SERIAL might eventually run out of values.

Solution: Use BIGSERIAL for tables expected to have millions of rows.

Summary

PostgreSQL serial types provide a convenient way to create auto-incrementing columns that are perfect for primary keys and other ID columns. They automatically create and manage sequences, simplifying database design and maintenance.

Key points to remember:

  • Use SERIAL for most tables
  • Use BIGSERIAL for tables that may contain millions of rows
  • Consider IDENTITY columns for newer PostgreSQL versions (10+)
  • Be aware of sequence management during data imports or when explicitly setting values
  • Serial values may have gaps, which is usually not a problem

Practice Exercises

  1. Create a books table with an auto-incrementing ID, title, author, and publication year.
  2. Insert five books without specifying IDs and verify they received sequential IDs.
  3. Insert a book with ID 100, then insert another book without specifying an ID. What happens?
  4. Reset the sequence to avoid conflicts after your explicit insert.
  5. Create two related tables (such as authors and books) using serial primary keys and foreign key relationships.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)