WEEK 01 - KEY LOOKUP

The index was useful. But incomplete.

A scheduled stored procedure. 194,000 executions. Each one doing 2,835 logical reads.

After one INCLUDE column added to the index, the same query dropped to 10 logical reads.

THE SYMPTOM

High CPU. No single catastrophic query. Just a small inefficiency repeated 194,000 times.

THE EXECUTION PLAN

The plan had three nodes. Index Seek → Nested Loops → Key Lookup.

The Key Lookup was executing 920 times per stored procedure call.
Each one a round-trip back to the clustered index.

THE PATTERN

When you see Index Seek → Nested Loops → Key Lookup, stop and check.

Plan node

Meaning

Index Seek

The index to rebuild

Nested Loops

Output List → columns to add to INCLUDE

Key Lookup

Node cost % = optimization potential

THE ROOT CAUSE

The index covered (CompanyID, MovementID). The query also needed TransactionCode. Not in the index. Not in the INCLUDE list. Just missing.

920 lookups per execution. 194,000 executions. All to fetch one column that could have been right there.

THE FIX

CREATE NONCLUSTERED INDEX IX_SalesTransaction_Company_Movement
    ON dbo.SalesTransaction
    (
        CompanyID,
        MovementID
    )
    INCLUDE
    (
        TransactionCode
    )
    WITH (DROP_EXISTING = ON);

The only change: adding TransactionCode to the INCLUDE clause. The index key stayed the same. SQL Server no longer needed to visit the clustered index to fetch that column.

THE NUMBERS

Metric

Value

Logical reads before

2,835

Logical reads after

10

Reduction

99.6%

The query was executed 194,000 times. A small per-execution cost became real CPU pressure.

UNDER THE HOOD

A nonclustered index is a sorted copy of a column subset.
Every leaf node carries the clustering key.
When a query needs a column not in the index, SQL Server uses that key to jump back to the clustered index, once per row, inside a Nested Loop.
That jump is the Key Lookup.

No clustered index? The lookup becomes a RID Lookup. Same problem, same fix.

WATCH OUT

Do not add INCLUDE columns blindly. Every included column increases index size and write overhead.

Before changing an index, check: execution frequency, logical reads before/after, read/write ratio, overlapping indexes, deployment impact.

Same key columns on multiple indexes? Consolidate — one index, same key, union of all useful INCLUDE columns.

THE LESSON

The index was not wrong. It was incomplete. One missing column caused 920 Key Lookups per execution. Added to INCLUDE. Done.

Pier Giuseppe Mandosi
SQL Server Performance Diaries

Keep Reading