Hook: Interviewers love this because it looks simple, but it reveals whether you know how databases create safe unique IDs under concurrency.
Question: What are AUTO_INCREMENT and IDENTITY in SQL?
Answer: They are database features that generate a new numeric value for a column when you insert a row. The usual purpose is to create a surrogate key, which is an artificial ID with no business meaning, such as an order id or user id. The important idea is that the database, not your application, safely picks the next value.
Interview-Ready Answer: AUTO_INCREMENT and IDENTITY are automatic number generators for columns, usually primary keys. I use them when I want the database to assign a unique id on insert, which avoids race conditions from application code doing MAX(id)+1. One useful detail is that these values are not guaranteed to be gapless, because rollbacks, deletes, and caching can leave holes.
Detailed Explanation: The core idea is very small: one column is marked as "auto-generated," and the database fills it in during INSERT if you do not provide a value. In practice, this is used for a primary key, but technically it can be a unique surrogate key even if it is not the primary key.
Think of it like a coat-check ticket dispenser: every time someone arrives, the machine hands out the next ticket number. If someone leaves the line or the machine prints a ticket that is never used, that number is still gone.
| Feature | MySQL AUTO_INCREMENT | SQL Server IDENTITY | PostgreSQL IDENTITY |
|---|---|---|---|
| Standard? | No | No | Closer to standard |
| Typical use | Primary key | Primary key | Primary key |
| Override? | Yes | Yes | Yes, by rule |
| Older style | Auto number | Identity column | SERIAL |
In simple words, the behavior is the same idea across engines, but the syntax and rules differ. IDENTITY is the SQL-standard-style concept, while AUTO_INCREMENT is the MySQL name for it. PostgreSQL also supports identity columns, and older PostgreSQL code often used SERIAL, which is a convenience shortcut built on a sequence.
UUID or a distributed ID scheme is often better.Performance notes: assigning the next value is typically O(1), because the engine just grabs the next number from its generator. The insert still pays the normal cost of writing the row and updating indexes, so the key generation itself is not the expensive part. Identity columns are also compact: INT uses 4 bytes and BIGINT uses 8 bytes per row, which is much smaller than a 36-character UUID.
INT maxes out at about 2.1 billion; a busy system should often use BIGINT.BY DEFAULT or ALWAYS.Memory rule: if the app is asking, "What number should I use next?" the app is doing the database’s job.
Real-World Example: Imagine a checkout service for an online store. Every order needs an internal order id, and the team uses an identity column so the database assigns the id safely even when hundreds of customers check out at the same second.
What goes wrong when people misunderstand this? A developer replaces the identity column with application code that does MAX(order_id) + 1 before inserting. Under load, two requests read the same maximum value, both try to insert the same next id, and one fails with a duplicate-key error. Users see failed checkouts, logs fill with unique constraint violations, and support tickets say "payment succeeded but order confirmation never appeared."
A related bug happens when a team expects ids to be continuous. They see order 1042 and then 1045, assume orders were lost, and start a fake incident. In reality, 1043 and 1044 were reserved and then rolled back during failed transactions. The lesson is that identity numbers are for uniqueness, not for counting business events.
-- PostgreSQL demo: identity columns generate ids safely, but gaps and overrides are still possible.
-- This script is runnable as-is on PostgreSQL.
DROP TABLE IF EXISTS orders_always;
DROP TABLE IF EXISTS orders_default;
-- BY DEFAULT means the database fills in the id unless you provide one.
CREATE TABLE orders_default (
order_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
customer_name TEXT NOT NULL,
amount NUMERIC(10,2) NOT NULL CHECK (amount > 0)
);
INSERT INTO orders_default (customer_name, amount)
VALUES
('Ada', 19.99),
('Ben', 42.50);
-- Explicit ids are allowed here, so the app can override if it has a strong reason.
INSERT INTO orders_default (order_id, customer_name, amount)
VALUES (1000, 'Carla', 9.99);
SELECT 'orders_default' AS table_name, order_id, customer_name, amount
FROM orders_default
ORDER BY order_id;
-- ALWAYS means the database owns the column unless you explicitly override it.
CREATE TABLE orders_always (
order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_name TEXT NOT NULL
);
INSERT INTO orders_always (customer_name)
VALUES ('Dina');
-- This failing insert shows the protection: you cannot sneak in your own id by accident.
DO $$
BEGIN
BEGIN
INSERT INTO orders_always (order_id, customer_name)
VALUES (2000, 'Eli');
EXCEPTION
WHEN others THEN
RAISE NOTICE 'Expected failure for GENERATED ALWAYS AS IDENTITY: %', SQLERRM;
END;
END $$;
-- If you truly need to override, PostgreSQL requires an explicit clause.
INSERT INTO orders_always (order_id, customer_name)
OVERRIDING SYSTEM VALUE
VALUES (2000, 'Eli');
SELECT 'orders_always' AS table_name, order_id, customer_name
FROM orders_always
ORDER BY order_id;
-- Edge case: gaps are normal. A rolled-back insert can consume a number.
BEGIN;
INSERT INTO orders_default (customer_name, amount)
VALUES ('Failed Order', 1.00);
ROLLBACK;
-- The next successful insert may skip the burned value, which is expected behavior.
INSERT INTO orders_default (customer_name, amount)
VALUES ('Fiona', 7.77);
SELECT 'after rollback' AS table_name, order_id, customer_name, amount
FROM orders_default
ORDER BY order_id;Follow-up & Tricky Questions:
GENERATED BY DEFAULT and GENERATED ALWAYS? BY DEFAULT lets you supply your own value; ALWAYS blocks manual values unless you explicitly override them. That makes ALWAYS safer when the database must control the id.INT or BIGINT? Use BIGINT if you expect long life or high volume. A 4-byte INT tops out around 2.1 billion, which can be too small for large systems.BY DEFAULT or ALWAYS.AUTO_INCREMENT standard SQL? No. It is MySQL-specific wording; IDENTITY is the more standard term.Common Mistakes:
MAX(id)+1 in application code. Fix: let the database generate the value so concurrent requests do not collide.INT for a table that will grow forever. Fix: choose BIGINT early if the row count may become huge.Memory Hook: Think of identity like a deli ticket machine: the store hands out the next number, not the customer. The number is for service order, not for meaning, and some tickets are skipped when people leave or transactions fail.
Cheat Sheet:
AUTO_INCREMENT and IDENTITY both mean auto-generated ids.BY DEFAULT allows manual ids; ALWAYS blocks them unless overridden.BIGINT when long-term scale matters.Practice Tasks:
BY DEFAULT to ALWAYS and test what manual inserts do.