Menu
Try in Playground

SQLite Primary Keys: INTEGER, Composite, and AUTOINCREMENT

How primary keys work in SQLite — the special INTEGER PRIMARY KEY, composite keys, AUTOINCREMENT, and the quirks that catch newcomers.

This page includes runnable editors — edit, run, and see output instantly.

What a Primary Key Actually Does

A primary key is the column (or combination of columns) that uniquely identifies each row in a table. Two rows can't share the same primary key value. SQLite enforces that for you, and it uses the key to find rows quickly.

The simplest form goes inline on a column:

You didn't supply an id and SQLite filled one in. That's not magic — it's a special case for INTEGER PRIMARY KEY that's worth understanding before you write anything else.

INTEGER PRIMARY KEY Is Special

In most databases, a primary key is just a unique index. In SQLite, every regular table already has a hidden 64-bit integer called the rowid that identifies rows internally. When you declare a column as exactly INTEGER PRIMARY KEY, that column becomes the rowid. No extra index, no extra storage — your id and the row's physical location are the same thing.

id and rowid are the same column under two names. Lookups by id go straight to the row — there's no second tree to walk. This is why the standard advice for SQLite is: if you want a numeric primary key, write INTEGER PRIMARY KEY exactly. Not INT, not BIGINT, not INTEGER NOT NULL PRIMARY KEY (well, that one works, but the type must be INTEGER).

Other types still work — they just get a separate unique index, which is fine, just not as compact.

AUTOINCREMENT Is Usually Unnecessary

A common reflex from other databases is to write id INTEGER PRIMARY KEY AUTOINCREMENT. In SQLite, the AUTOINCREMENT keyword does something narrower than the name suggests, and most of the time you don't need it.

Without AUTOINCREMENT, an INTEGER PRIMARY KEY column auto-fills with one more than the largest existing rowid. If you delete the last row, the next insert may reuse that id.

With AUTOINCREMENT, SQLite tracks the highest id ever used in a side table called sqlite_sequence and never reuses values, even after deletion.

The plain table reused id 3. The AUTOINCREMENT table jumped to 4. Unless you have a real reason to forbid id reuse — auditing, external references that linger after deletion — skip AUTOINCREMENT. It costs an extra write per insert and a separate bookkeeping table.

Composite Primary Keys

Sometimes a single column isn't enough. A join table that maps users to roles, for example, is uniquely identified by the pair (user_id, role_id). For that, declare the key at the table level:

The pair has to be unique across the whole table — (1, 10) can only appear once. Either column on its own can repeat freely. That's the whole point: each user can have many roles, each role can have many users, but a given user-role pairing exists at most once.

A composite primary key creates a separate index covering the listed columns. It does not become the rowid — only a single INTEGER PRIMARY KEY gets that treatment.

The NULL-in-Primary-Key Footgun

Here's a quirk that surprises people coming from PostgreSQL or MySQL: in a regular SQLite table, a primary key column other than INTEGER PRIMARY KEY can contain NULLs. It's a long-standing bug that the SQLite authors kept for backward compatibility.

Two rows with NULL slipped past the primary key. The fix is to add NOT NULL explicitly on every non-integer primary key column:

Or use a STRICT table, where the NULL-in-PK bug is corrected. The habit of writing NOT NULL on every primary key column is cheap insurance.

Primary Key vs UNIQUE

Both prevent duplicates. The differences:

  • A table has at most one primary key, but can have many UNIQUE constraints.
  • The primary key is the table's "main" identifier — foreign keys point at it by default.
  • An INTEGER PRIMARY KEY becomes the rowid; a UNIQUE integer column does not.
  • UNIQUE columns happily accept multiple NULLs (each NULL is considered distinct).

id is the row's identity. email and username are also unique, but they're business attributes — they could change, while the id shouldn't.

Adding a Primary Key Later (Mostly: Don't)

SQLite's ALTER TABLE is limited. You can't run ALTER TABLE ... ADD PRIMARY KEY — that statement doesn't exist. If you forgot the primary key and the table already has data, the path is to recreate it:

This is the standard SQLite migration dance. Wrap it in a transaction in real code, and disable foreign keys briefly if other tables reference this one. The lesson: get the primary key right at CREATE TABLE time.

A Quick Checklist

When you write a new table, ask:

  • Does this row have a natural unique id? If it's a single integer, use INTEGER PRIMARY KEY.
  • Is the identity actually a combination of columns (a join table)? Use a table-level PRIMARY KEY (col_a, col_b).
  • Is the key text or another non-integer type? Add NOT NULL explicitly.
  • Do you actually need AUTOINCREMENT? Probably not.
  • Is the table small and read-mostly with a non-integer primary key? Consider WITHOUT ROWID (covered with the rowid doc).

Next: rowid

INTEGER PRIMARY KEY made a brief appearance as "an alias for rowid" — but rowid is the foundation under every regular SQLite table, and it's worth understanding directly. That's the next page.

Frequently Asked Questions

How do I define a primary key in SQLite?

Add PRIMARY KEY to a column in your CREATE TABLE statement, e.g. id INTEGER PRIMARY KEY. For a key that spans multiple columns, use a table-level constraint: PRIMARY KEY (col_a, col_b). The column or combination must be unique across rows.

What's the difference between INTEGER PRIMARY KEY and other primary keys in SQLite?

INTEGER PRIMARY KEY is special: it becomes an alias for the table's built-in rowid, so it's stored directly in the B-tree with no extra index. Any other type, or a composite key, gets a separate unique index. For single-column numeric ids, INTEGER PRIMARY KEY is faster and smaller.

Do I need AUTOINCREMENT on a SQLite primary key?

Usually no. An INTEGER PRIMARY KEY already auto-assigns a unique rowid when you insert NULL. AUTOINCREMENT only adds a guarantee that ids never get reused after deletion — at the cost of an extra sqlite_sequence table. Skip it unless you specifically need that monotonic-id behavior.

Why does my SQLite primary key allow NULL values?

Historical bug, preserved for compatibility: in regular tables, a non-INTEGER primary key column can accept NULLs unless you explicitly add NOT NULL. The INTEGER PRIMARY KEY column is the exception — it never allows NULL. To be safe, write NOT NULL on every primary key column, or use a STRICT table where the rule is enforced properly.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED