Clustered vs Nonclustered Indexes in SQL Server: The B-Tree Explained

Written by

in

Clustered vs Nonclustered Indexes in SQL Server: The B-Tree Explained

Every index recommendation you’ve absorbed passively up to this point (“add an index here”) gets its real mechanical foundation in this chapter. Every SQL Server index is a B-tree: a root page, branch pages, and leaf pages. The difference between clustered and nonclustered is what lives at the leaf level — and that single difference explains almost everything else in this chapter.

Clustered = The Table Itself(nonclustered is a separate, narrow lookup)CLUSTERED INDEXleaf level = the data itselfid 101 — data rowid 102 — data rowid 103 — data rowid 104 — data rowid 105 — data rowphysically stored in this exact orderat most ONE per tableNONCLUSTERED INDEXleaf level = key + pointer onlystatus=’shipped’ → id 101status=’cancelled’ → id 104status=’shipped’ → id 103MANY nonclustered allowed per tablekey lookup →Gotcha: many key lookups can cost more than one full scan —that’s exactly when the optimizer abandons the index. 📌

The B-Tree, Visualized

Root: 1-50000 Branch: 1-16666 Branch: 16667-33333 Branch: 33334-50000 Leaf pages — actual data rows, in key order

A seek walks root → branch → leaf, typically just 3-4 page reads even against a table with millions of rows — this is the entire reason indexes matter: it turns “read every row” into “read a handful of pages,” a logarithmic rather than linear cost as the table grows.

Clustered vs Nonclustered

Clustered Nonclustered
Leaf level contains The actual data rows Key + pointer back to clustered key
Per table At most one Many allowed
Created by default via PRIMARY KEY (Fundamentals Ch.5) Nothing — explicit CREATE INDEX

This is worth internalizing precisely: a clustered index doesn’t sit “alongside” the table — for a clustered table, the table is the index. There’s no separate copy of the data; the rows are physically stored in clustered-key order. A nonclustered index, by contrast, is a genuinely separate structure, small and narrow, that only stores its key columns plus a pointer back.

The Key Lookup Problem

A query that filters on a nonclustered index’s column but selects other columns not in that index requires a key lookup — jumping from the nonclustered leaf back to the clustered index to fetch the rest. For a handful of rows this is cheap; for a large result set, SQL Server often abandons the index entirely and scans the whole table, because thousands of individual lookups cost more than one sequential scan.

-- Confirm this tipping-point behavior yourself
CREATE NONCLUSTERED INDEX IX_OrderLog_Status ON dbo.OrderLog (order_status);

-- Selective (few matching rows): optimizer uses the index + key lookups
SELECT * FROM dbo.OrderLog WHERE order_status = 'cancelled'; -- rare status, few rows

-- Unselective (most rows match): optimizer likely abandons the index for a scan
SELECT * FROM dbo.OrderLog WHERE order_status = 'completed'; -- common status, most rows
-- Compare the two actual execution plans (Ctrl+M) to see the optimizer's choice change
Practice tip: Run both queries above with the actual execution plan visible, and hover over each Seek/Scan operator to read its estimated row count and cost percentage. Seeing the optimizer switch strategies based purely on how selective the filter is — not on anything about the index itself — is the single most useful intuition this lesson can give you before Chapter 3’s execution plan reading lesson goes deeper.

Enjoyed this?

Subscribe to get every new SQL Server lesson as soon as it’s published, and share it with a developer who’d find it useful.

📡 Subscribe via RSS  | 
Share on X  | 
Share on LinkedIn  | 
Share on Facebook

Want the full structured course with quizzes, projects, and 10+ exercises per chapter? Check out SQL Server for Developers & DBAs, coming soon on this site.