Skip to content

Clustered vs Non-Clustered Index

Clustered Index

The table data is physically sorted and stored in the order of the index key. The index IS the table. Leaf nodes of B+ Tree contain ACTUAL ROW DATA. Only ONE clustered index per table.

Clustered Index on ID:

Leaf Node 1        Leaf Node 2        Leaf Node 3
id=1 | full row --> id=4 | full row --> id=7 | full row
id=2 | full row     id=5 | full row     id=8 | full row
id=3 | full row     id=6 | full row     id=9 | full row

  actual row data lives inside leaf nodes

Non-Clustered Index

A separate structure that stores the key + a pointer back to the actual row. Index is separate from the table. Leaf nodes store key + pointer (RID or primary key). Multiple non-clustered indexes per table.

Non-Clustered Index on Email:

Index B+ Tree              Actual Table (heap)
alice@ --> ptr ---------> id=3, alice@, age=25
bob@   --> ptr ---------> id=1, bob@, age=30
carol@ --> ptr ---------> id=5, carol@, age=22

MySQL InnoDB Specifics

  • Primary Key → always clustered index → leaf nodes store full row data
  • Secondary Keys → non-clustered → leaf nodes store the PRIMARY KEY (not row ptr)
  • SELECT via secondary index → Two B+ Tree traversals! (secondary index → PK → clustered index)

Tip: This is why choosing a small primary key (int vs UUID) matters in MySQL — every secondary index stores it.

Covering Index — Avoiding Double Lookup

sql
-- Query only needs id and email
SELECT id, email FROM users WHERE email = 'bob@gmail.com'
-- If index is on (email, id) → index already has both columns
-- No need to go back to the table at all!
-- Called a "covering index"

Covering Index — How One Lookup Works

Covering index leaf nodes DO still store the pointer — but the pointer is NEVER FOLLOWED because all needed columns are already in the leaf node itself. The table is never accessed, which is what makes it a single-structure lookup.

Normal non-clustered leaf node:
| user_id = 5 | ptr → row |   <-- must follow ptr (disk read)

Covering index leaf node:
| user_id = 5 | status = active | amount = 500 | ptr |
                                                  ^-- exists but IGNORED
  everything already here -- ptr never followed

Covering Index is a Non-Clustered Index

A covering index is NOT a separate index type. It's a PROPERTY of an index relative to a query. The same index can be covering for one query and non-covering for another.

ClusteredNon-ClusteredCovering Index
Data storageInside leaf nodesSeparateSeparate (+ extra cols)
Per tableOne onlyManyMany
Table access neededNeverYesNo (for covered queries)
Role/TypeTypeTypeRole (not a type)