Execution walkthrough

What SQL Server reads for one key-lookup plan

SELECT * FROM t WHERE t.secondary_id = 4;
Clustered index PK_t (id int) Nonclustered IX_t_secondary_id, non-unique, no INCLUDE Both trees 3 levels Histogram estimate 3 rows
Nested Loops (Inner Join)← Index Seek · IX_t_secondary_id+ Key Lookup (Clustered) · PK_t

Reading the diagram

Each box is one 8 KB page

page 1:900 is the page address: file 1, page 900. Child pointers inside other pages use exactly this address.

  • root is the single entry point. Its address is stored in the system catalog.
  • int (intermediate) pages only route the search downward.
  • leaf pages hold the actual index entries. The double lines between leaves are the previous/next page pointers.

Rows in IX_t_secondary_id

The index is non-unique, so SQL Server appends the clustering key id. Every entry becomes a pair (secondary_id, id), sorted by secondary_id first, then id.

RowMeaning
(4,17)Leaf entry. A row with secondary_id 4 exists, and its id is 17.
(3,5120) → 1:902Separator. Keys from (3,5120) upward live under page 1:902.
∅ → 1:901First pointer on the page. It has no lower bound and covers keys below the next separator.

Rows in PK_t (clustered)

The key is just id. The leaf level is the table itself, so each leaf row is a complete row.

RowMeaning
17 ▪ full rowEvery column of the row with id 17.
5000 → 1:502Separator. ids from 5000 upward live under page 1:502.
∅ → 1:501ids below 5000 live under page 1:501.

Why the seek starts at (4, −∞)

The query fixes only the first half of the key. All entries for secondary_id = 4 sit next to each other in key order, but the query cannot know which id comes first. Here is the whole leaf level laid out in sorted order:

(1,8)1:910(1,330)1:910(2,12)1:910 (2,40)1:911(2,77)1:911(3,5)1:911 (3,5120)1:912 start
(4,−∞)
(4,17)1:912(4,203)1:912(4,9551)1:913 stop (5,88)1:913(5,412)1:913
  1. The seek needs the position just before the first 4. Written as a pair, that position is "secondary_id 4, with an id smaller than any real id", which is (4, −∞). −∞ is not stored anywhere. It is notation for "lowest possible value of the unknown column".
  2. Pairs compare left to right, like words in a dictionary. The first column decides unless it ties, and only then does the second column matter. So (4, −∞) is greater than (3, anything) and less than (4,17) and every other real (4, …).
  3. The seek descends to that position, then reads forward until the first column exceeds 4.

In the actual execution plan, the Index Seek's properties show Seek Keys[1]: Prefix: secondary_id = 4. A "prefix" seek on the leading key column is the same operation shown here as (4, −∞).

IX_t_secondary_id

key (secondary_id, id)

PK_t (clustered)

key (id) · leaf = full row
Click any row in either treeThe meaning of that row appears here.

Keyboard: ← and → step through.
IX reads0
PK reads0
Logical reads0
Rows returned
idsecondary_idother columns
None yet.

Where the plan flips to a scan

Same table at production scale: 2,000,000 rows, 20,000 clustered leaf pages, both trees 3 levels deep. Drag the match count and compare logical reads for each plan.

Matches Selectivity Lookups
Seek + key lookups
Clustered index scan
110010k1M

The bars use a log scale. Raw read parity is about 6,700 matches (0.33% of the table). The optimizer costs random lookup I/O higher than sequential scan I/O, so its real tipping point usually lands below that figure.