SQLite’s AUTOINCREMENT keyword is a bit of a red herring; most of the time, you don’t need it and ROWID is doing all the heavy lifting anyway.
Let’s see ROWID in action. Imagine we have a simple table for tracking user IDs:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL
);
When we insert data:
INSERT INTO users (username) VALUES ('alice');
INSERT INTO users (username) VALUES ('bob');
INSERT INTO users (username) VALUES ('charlie');
And then query it:
SELECT rowid, id, username FROM users;
The output will look something like this:
rowid id username
1 1 alice
2 2 bob
3 3 charlie
Notice that id and rowid are identical. This is because INTEGER PRIMARY KEY is an alias for ROWID. SQLite automatically creates a hidden ROWID column for every table that doesn’t explicitly declare a primary key. If you declare a column as INTEGER PRIMARY KEY, it’s essentially just a convenient way to access and manipulate the table’s ROWID.
The problem this solves is efficient record identification and management. Without a ROWID (or equivalent), SQLite would need to implement more complex mechanisms to uniquely identify rows, potentially impacting performance. ROWID provides a simple, fast, and guaranteed unique identifier for each row.
Internally, ROWID is an alias for a special column that SQLite manages. For tables without an INTEGER PRIMARY KEY, this ROWID is automatically assigned. When you declare an INTEGER PRIMARY KEY, that column is the ROWID. SQLite uses this ROWID for internal indexing and for operations like INSERT and SELECT. The ROWID is an 8-byte signed integer.
The key levers you control are how you define your primary key.
- No explicit
PRIMARY KEY: SQLite will create a hiddenROWIDcolumn, typically an alias for a 64-bit integer that starts at 1 and increments. INTEGER PRIMARY KEY: This column is theROWID. It will behave like the implicitROWIDand will auto-increment.INTEGER PRIMARY KEY AUTOINCREMENT: This is where things get subtle. It still usesROWIDinternally, but it modifies the sequence generation. Normally,ROWIDs are only guaranteed to be unique. They can be reused if a row is deleted.AUTOINCREMENTchanges this by ensuring that theROWIDassigned to a new row is always greater than anyROWIDpreviously used for that table, even after deletions. This comes with a performance penalty because SQLite must maintain a special table (sqlite_sequence) to track the highest assignedROWID.
The one thing most people don’t realize is that AUTOINCREMENT doesn’t change the underlying mechanism of ROWID generation; it only adds a constraint that the next ROWID must be strictly greater than all previous ROWIDs ever issued for that table. This means if you delete rows, their ROWIDs are not eligible for reuse with AUTOINCREMENT, whereas without AUTOINCREMENT, they could be reused.
The next concept you’ll grapple with is handling composite primary keys and their impact on ROWID behavior.