Step 1 of 3 · Reading · ~3 min
Building Secondary Indexes
Query Planner & Indexing
CREATE INDEX — Secondary B-Trees
Until now, the only column your engine could seek on efficiently is whatever it uses as the primary key/rowid. This lesson lets users declare a secondary index on any column, giving the query planner a fast path for lookups that don't touch the primary key at all.
The core idea: an index is just another B-tree
A secondary index is structurally the same B-tree you already built for the table's primary storage — it just maps a different key. Instead of rowid → row, it maps indexed_column_value → rowid:
CREATE INDEX idx_name ON users (name)
builds a B-tree keyed by name, where each leaf entry points at the rowid of the matching row in the underlying table:
B-tree keyed on `name`:
"Alice" -> rowid 3
"Bob" -> rowid 1
"Carol" -> rowid 7
To answer WHERE name = 'Alice', the planner now has an alternative to a full table scan: search this B-tree for "Alice" (O(log n)), get back rowid 3, then fetch that one row directly from the table (a single random-access lookup) instead of comparing every row's name column.
What you need to implement
- Parse
CREATE INDEX <idx_name> ON <table> (<column>)and respondOK. - Build the index eagerly at creation time: iterate every existing row in the table, insert
(column_value, rowid)into a fresh B-tree, and register it (e.g. in a dict keyed by table name → column name → B-tree, or table name → index name). - Keep the index in sync going forward: every
INSERT/UPDATE/DELETEon the indexed table needs a matching insert/update/delete in the index B-tree. An index that silently goes stale after the first write is a correctness bug, not just a performance one — a stale index can return wrong rows or miss rows entirely. - Wire it into the planner you built in the table-scan-vs-index-scan lesson: when a
WHEREpredicate references an indexed column, the planner should now choose the index scan path and report it via.explain:
.explain SELECT * FROM users WHERE name = 'Alice'
→ SEARCH TABLE users USING INDEX idx_name
Why "secondary" index specifically
"Primary" usually refers to the structure the table's rows are physically stored in (often the rowid/primary-key B-tree — this is what SQLite calls a "clustered" index). A secondary index is an additional structure layered on top, pointing back at the primary storage rather than holding the row data itself. That indirection is exactly why every secondary index lookup costs one extra hop (index seek, then row fetch) compared to a lookup that's serviced directly by the primary structure.
Edge cases to watch
- Creating an index on a table that doesn't exist, or on a column that doesn't exist, should be an error rather than silently creating garbage.
- Creating an index with a name that already exists — decide whether to reject or replace, and be consistent.
- Non-unique values: multiple rows can share the same indexed value (e.g. two users both named "Alice"), so each key in the index B-tree must be able to map to multiple rowids, not just one.
- Index maintenance cost is real: every write now touches N structures instead of 1 (the table plus each index on it) — this is the classic read/write tradeoff indices introduce, and it's why databases don't index every column by default.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…