powershelldba.de

SQL Server B-Tree: How an Index Seek Finds One Row

An Index Seek doesn't search the table — it walks a tree. Three or four page reads, and it's standing on the exact row, whether the table holds a thousand rows or a billion. Here's the structure that makes that possible, with an animated walk-through of the traversal.

What "Seek" Actually Means

"Index Seek" and "Index Scan" are both operators that read an index — the difference is how they get to the rows.

That distinction is the entire reason a seek is cheap: its cost is tied to the height of the tree, not the size of the table.

The Shape of a B-Tree Index

Every SQL Server index — clustered or nonclustered — is a balanced B-tree (technically a B+ tree). Three kinds of pages:

"Balanced" means every leaf is the same distance from the root. A table growing from a million to a billion rows doesn't multiply the page reads by a thousand — it typically adds one more level. That's the logarithmic growth that makes a seek scale so well: log(n), not n.

Watching a Seek Happen

Below is a simplified three-level tree. The query asks for a single key; watch how few pages it takes to land on it.

SEEK: WHERE OrderID = 61 Root page < 50 | 50–89 | ≥ 90 Intermediate 12, 28 Intermediate < 58 | 58–73 | ≥ 74 Intermediate 95, 120 Leaf page 50, 54 Leaf page 58, 61, 67 Leaf page 74, 80, 88 OrderID = 61 — found

Root → intermediate → leaf: three page reads to find one row, regardless of table size. Hover to pause.

That's the whole trick. At every level the engine does one comparison against the page's separator keys, follows the one pointer that can contain the value, and discards everything else on that page without reading it. Two levels of separators here means the table could hold anywhere from a few hundred rows to several million and the seek would still take exactly three page reads.

Seeing It in the Execution Plan

The animation above maps directly onto what SQL Server reports for a real query:

CREATE TABLE dbo.Orders
(
    OrderID    INT IDENTITY PRIMARY KEY,
    CustomerID INT NOT NULL,
    OrderDate  DATE NOT NULL,
    Status     TINYINT NOT NULL
);

CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
    ON dbo.Orders (CustomerID)
    INCLUDE (OrderDate, Status);

SET STATISTICS IO ON;

SELECT OrderDate, Status
FROM dbo.Orders
WHERE CustomerID = 4821;

With a reasonably sized table, the plan shows an Index Seek on IX_Orders_CustomerID, and STATISTICS IO reports a handful of logical reads — root, one or two intermediate levels, one leaf page. Grow the table by a factor of 100 and that number barely moves.

Read the Seek Predicate, not just the operator name. Click the Index Seek operator and check its properties: Seek Predicate lists what was actually used to navigate the tree (the part that limits page reads). Predicate (without "Seek") lists a residual filter evaluated after landing on a page — it doesn't reduce how many pages were read. An operator can say "Index Seek" and still scan a wide range of leaf pages if the seek predicate only narrows things down loosely.

What Turns a Seek Into a Scan

The optimizer can only use the tree structure if the predicate is SARGable — expressible as "the leading index key(s) fall in this range/equal this value" without first computing something on every row. Common ways a seek quietly becomes a scan:

Seek vs. Scan, Side by Side

Aspect Index Seek Index Scan
Page reads Roughly the tree height — O(log n) Every leaf page in scope — O(n)
Requires SARGable predicate on the leading key(s) No usable predicate, or optimizer decides scanning is cheaper
Scales with table growth Barely — adds a tree level roughly every order of magnitude Linearly — twice the rows, twice the reads
Typical for Point lookups, narrow ranges, joins on indexed keys Aggregations over most of the table, small tables, no filter

When a Scan Is Actually Fine

Don't chase "Seek" as a goal in itself. A scan is the right choice — and often the cheaper one — when:

The query optimizer already weighs this with cost-based estimates; if it picked a scan on a table you expected a seek on, look at the estimated row count and the predicate first, before assuming the plan is "wrong."

Best Practices

The Bottom Line

A B-tree index turns "find this row" from a linear search into a small, fixed number of comparisons — because the tree is balanced and every level narrows the search by following exactly one pointer. An Index Seek is SQL Server using that structure the way it was built to be used; an Index Scan is SQL Server giving up on it, deliberately or otherwise.

When a query is slower than it should be, the plan's Seek Predicate — not the operator's name — is where the real answer lives.