25. Indexing¶
Every query you have written since Unit 2 eventually has to touch physical storage: rows
sitting in disk blocks, read one block at a time. So far we have quietly assumed the DBMS
finds the right rows efficiently — but ask yourself what actually has to happen to answer
WHERE propertyNo = 'PA14' against a table of ten million properties, with nothing extra
to help. Without help, the DBMS has exactly one option: check every single row. Indexing
is the technique that avoids this, and it is arguably the single most important physical
design decision a database administrator makes, because the right index can turn a query
that takes minutes into one that takes microseconds — with no change to the query itself.
We continue the PropertyForRent relation from the DreamHome case study introduced in
earlier lectures.
In This Lecture¶
- Why a table with no index forces a full table scan for every lookup
- Primary indexes — built on the ordering key of a physically ordered file
- Secondary indexes — built on a non-ordering field, for queries the file isn't sorted by
- Dense vs. sparse indexes — one entry per record vs. one entry per block
- Single-level vs. multi-level indexes — why big indexes need to be indexed themselves
- How to choose which columns are actually worth indexing
- The real trade-off: faster reads against slower writes and extra storage
- Real
CREATE INDEXSQL syntax
The Cost of Not Having an Index¶
Consider PropertyForRent with a few sample rows:
| propertyNo | street | city | type | rooms | rent | branchNo |
|---|---|---|---|---|---|---|
| PA14 | 6 Lawrence St | Islamabad | House | 5 | 42000 | B007 |
| PL94 | 22 Mall Rd | Karachi | Flat | 3 | 28000 | B005 |
| PG4 | 18 Dale Rd | Lahore | Flat | 3 | 21000 | B003 |
| PG36 | 2 Manor Rd | Lahore | Flat | 4 | 26000 | B003 |
| PG21 | 5 Novar Dr | Lahore | House | 5 | 39000 | B003 |
Five rows is nothing — a query like SELECT * FROM PropertyForRent WHERE propertyNo = 'PG21'
scans all five in a blink no matter how the DBMS looks for it. Now imagine the same table
holding 10,000,000 properties spread across 500,000 disk blocks. With no index, finding
PG21 means a full table scan: read block 1, check every row in it, read block 2, check
every row in it, and so on — on average reading half the table, and in the worst case (the
row isn't there, or it's the very last one) reading all 500,000 blocks.
Without an index — full table scan
Read block 1Check every row — no match
Read block 2Check every row — no match
… up to block 500,000Worst case: read every block
With an index — direct lookup
Look up PG21 in the indexA handful of comparisons, not 500,000
Follow the pointerJump straight to block 214,009
An index is exactly what the comparison above suggests: a small, separately stored structure that maps values of a chosen field (or fields) directly to the disk block (or exact row) that holds them — so the DBMS can jump to the answer instead of scanning for it. The price is that the index itself takes disk space and has to be kept up to date every time the underlying table changes; the rest of this lecture is about that trade-off, and about the different shapes an index can take.
Primary Indexes¶
A primary index is built on the field that the data file is physically ordered by — almost always the primary key, since that's the field most systems choose to sort the file on when one is needed. Because the file is already sorted on this field, the index only needs one entry per distinct key value, pointing to the first record with that value (or to the block that contains it).
| Index entry: propertyNo | Pointer |
|---|---|
| PA14 | → block 1 |
| PG4 | → block 2 |
| PG21 | → block 2 |
| PG36 | → block 3 |
| PL94 | → block 3 |
A primary index assumes an ordered file
This only works because PropertyForRent is physically stored in propertyNo order.
If the file were unordered — the far more common case in practice — there is no
"primary index" possible on it at all; you'd need a different kind of index (a
clustering index, out of scope here, or the secondary index below) to speed up
lookups on that field.
Secondary Indexes¶
Most useful queries do not filter on the field a file happens to be ordered by. "Find all
properties in Lahore" filters on city — a field PropertyForRent is not sorted by. A
secondary index solves exactly this: it is built on a non-ordering field, and it must
have one entry for every record, because unlike a primary index it cannot rely on nearby
values being physically adjacent on disk — matching rows could be scattered across the entire
file.
| Index entry: city | Pointer |
|---|---|
| Islamabad | → PA14 |
| Karachi | → PL94 |
| Lahore | → PG4 |
| Lahore | → PG36 |
| Lahore | → PG21 |
Notice Lahore appears three times — once per matching record — because a secondary index
cannot compress multiple rows into one entry the way a primary index can. This is the direct
cause of the dense-vs-sparse distinction below.
Dense and Sparse Indexes¶
- A dense index has one index entry for every record in the file, whether or not the file is ordered on that field. Every secondary index is necessarily dense.
- A sparse index has one index entry per block (not per record), storing only the first key value in each block. This is only possible when the file is physically ordered on the indexed field, because then the DBMS can find any record by locating its block through the sparse index and scanning just that one block.
Dense index 1 entry / record
PA14 → record 1
PG4 → record 2
PG21 → record 3
PG36 → record 4
PL94 → record 5
Sparse index 1 entry / block
PA14 → block 1holds PA14
PG4 → block 2holds PG4, PG21
PG36 → block 3holds PG36, PL94
A sparse index is smaller and therefore faster to search than a dense one over the same file — but it can only ever be built on a field the file is physically ordered by, which is why a table typically has at most one sparse (primary) index but can have many dense (secondary) indexes.
Single-Level and Multi-Level Indexes¶
Everything above was a single-level index: one flat list of (key, pointer) pairs. That works fine while the index itself is small enough to search quickly — but an index over ten million rows is itself a large file. If the index no longer fits comfortably in memory, searching it starts to suffer from the same problem the index was built to solve in the first place.
The fix is to apply the same idea recursively: build an index on the index. This produces a multi-level index — the structure underlying the B-tree and B+-tree indexes that every production DBMS (MySQL's InnoDB, PostgreSQL, SQL Server, Oracle) actually uses internally.
Multi-level index — index the index, as many times as needed
Top-level (second-level) index — small enough to fit in one block; one entry per block of the level below
First-level index — one entry per data block, exactly like the single-level index above
Data file — the actual PropertyForRent rows, in disk blocks
A lookup now walks down the levels — check the top-level index (one block), follow its pointer to the right first-level block, follow that pointer to the right data block — instead of scanning a single enormous flat index. A B+-tree is precisely this idea made self-balancing: it keeps every level the same depth automatically as rows are inserted and deleted, guaranteeing that any lookup touches only a handful of blocks even as the table grows into the billions of rows.
This is why index lookups are described as O(log n)
Each level of a balanced multi-level index divides the search space by roughly the same
factor (the number of entries that fit in one block). That's exactly what gives B-tree
indexes their logarithmic lookup cost, in sharp contrast to a full table scan's linear
O(n) cost — the same O(1)-vs-O(n) gap from the data structures course, now showing up
inside the database engine itself.
Creating Indexes in SQL¶
In practice, you never build or maintain index structures yourself — the DBMS does it, once you tell it which column(s) to index:
-- A simple secondary index on one column
CREATE INDEX idx_property_city
ON PropertyForRent (city);
-- A unique index -- also enforces that no two rows may share a value
CREATE UNIQUE INDEX idx_property_no
ON PropertyForRent (propertyNo);
-- A composite (multi-column) index -- useful when queries filter on both columns together
CREATE INDEX idx_property_city_rent
ON PropertyForRent (city, rent);
-- Removing an index that is no longer earning its cost
DROP INDEX idx_property_city;
Most relational engines automatically create an index behind every PRIMARY KEY and often
every UNIQUE constraint — you are only responsible for the extra indexes your query
workload needs.
Index Selection and Performance¶
Not every column deserves an index — an index that's never used still costs storage and slows every write. Good candidates share these properties:
- High selectivity — the column has many distinct values, so an index lookup narrows the
search dramatically (
propertyNo,email). A column likeisActive(only two possible values) barely benefits, since an index lookup still returns roughly half the table. - Foreign keys — almost always worth indexing, since they're constantly used in joins.
- Columns in
WHEREclauses — filters your application runs often and against large tables are the clearest case for an index. - Columns in
ORDER BY— an index on the sort column can let the DBMS skip a separate sort step entirely. - Columns in
JOINconditions — exactly where a foreign-key index pays off most visibly.
Indexing everything is not a shortcut to a fast database
Every index the DBMS maintains must be updated on every INSERT, UPDATE, or DELETE
that touches the indexed column — so an over-indexed table can make writes dramatically
slower without a matching benefit, since most of those indexes are never actually used by
a query. Index selection is a genuine design decision, made from the query workload the
application actually runs, not a rule of "more indexes are always better."
Advantages and Disadvantages of Indexing¶
Advantages
Much faster readsO(log n) lookups instead of O(n) scans
Faster sorting and joinsAn indexed sort order can avoid a separate sort step
Enforces uniquenessUNIQUE indexes double as a constraint
Disadvantages
Slower writesEvery INSERT/UPDATE/DELETE must also update every affected index
Extra storageAn index over a large table can itself be sizeable
Maintenance overheadIndexes can fragment and need periodic rebuilding
Key Takeaways¶
- With no index, the DBMS must run a full table scan — checking every row — for any lookup that isn't handled another way; the cost grows linearly with table size.
- A primary index is built on the field a file is physically ordered by, and needs only one entry per distinct key value; a secondary index is built on any other field and must be dense (one entry per record), since matching rows aren't physically grouped.
- Sparse indexes (one entry per block) are only possible on an ordered file's primary index; every secondary index is necessarily dense.
- A single-level index that itself grows too large to search quickly is solved by building a multi-level index — an index on the index — which is exactly the idea behind the B-tree/B+-tree structures every production DBMS uses internally.
- Choose indexes based on real query workload: high-selectivity columns, foreign keys, and
columns that appear in
WHERE,JOIN, andORDER BYclauses — not every column. - Indexing is a genuine trade-off: faster reads and joins, at the cost of slower writes and extra storage — never "free" performance.
Indexing is the last physical-design tool in the relational toolbox this course covers before we widen the lens. The next three lectures step outside the relational model entirely, to see how a different family of databases — NoSQL — solves storage and lookup problems that don't fit neatly into rows and columns at all. Continue to Lecture 26 — Introduction to NoSQL Databases.