rowid, INTEGER PRIMARY KEY and AUTOINCREMENT

SQLite · Essentials · 10 min

Choosing the key form changes storage, id reuse and performance in ways specific to SQLite.

Every ordinary SQLite table has a hidden 64-bit integer key, the rowid. The table is stored as a B-tree ordered by rowid, so looking up a row by rowid is the fastest access there is. You can select it as rowid.

If you declare a column exactly as INTEGER PRIMARY KEY, that column becomes an alias for the rowid rather than a separate column. Insert NULL (or omit it) and SQLite assigns a value: normally one more than the current largest rowid. Note the exact spelling matters: INT PRIMARY KEY or BIGINT PRIMARY KEY is not an alias; it creates an ordinary column with a separate unique index.

Plain INTEGER PRIMARY KEY can reuse ids: if you delete the row with the largest id, the next insert may get the same id again. Adding AUTOINCREMENT guarantees ids are never reused, using a bookkeeping table sqlite_sequence. It costs a little extra work per insert; SQLite's docs recommend it only when non-reuse genuinely matters.

WITHOUT ROWID tables store rows in a B-tree ordered by the declared primary key instead. They save space and a lookup for tables with non-integer or composite primary keys and small rows, such as junction tables. They require an explicit PRIMARY KEY.

From Python, cursor.lastrowid returns the new rowid; in SQL, last_insert_rowid() or a RETURNING id clause.

Example

import sqlite3

con = sqlite3.connect(":memory:")
con.executescript('''
    CREATE TABLE a (id INTEGER PRIMARY KEY, v TEXT);              -- alias for rowid
    CREATE TABLE b (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT);  -- never reuses ids
    CREATE TABLE tag_link (post_id INT, tag TEXT, PRIMARY KEY (post_id, tag)) WITHOUT ROWID;
''')
for t in ("a", "b"):
    con.execute(f"INSERT INTO {t} (v) VALUES ('x'), ('y'), ('z')")
    con.execute(f"DELETE FROM {t} WHERE id = 3")
    new_id = con.execute(f"INSERT INTO {t} (v) VALUES ('w') RETURNING id").fetchone()[0]
    print(t, new_id)
# a 3    <- reused
# b 4    <- AUTOINCREMENT never reuses

print(con.execute("SELECT rowid, id FROM a WHERE id = 1").fetchone())   # (1, 1)
print(con.execute("SELECT * FROM sqlite_sequence").fetchall())          # [('b', 4)]

Remember

  • INTEGER PRIMARY KEY (exact spelling) aliases the rowid
  • Without AUTOINCREMENT, the largest deleted id can be reused
  • WITHOUT ROWID suits composite or non-integer keys with small rows

Watch out for

  • Declaring INT PRIMARY KEY and expecting automatic ids: only INTEGER PRIMARY KEY aliases the rowid.

Check yourself

  1. Which declaration makes id an alias for the rowid?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: sqlite.org/rowidtable.html