If you’ve ever run a SQL query that looks simple but takes longer than expected, one likely culprit is the Key Lookup. This often-overlooked operation can quietly slow things down—especially when you’re dealing with large datasets and high-frequency queries. In this blog post, we’ll explore what a Key Lookup is, how to spot it, and most importantly, how to eliminate it using a covering index. We’ll walk through a real-world demo and use actual execution plans to visualize the performance gain.
What’s a Key Lookup?
A Key Lookup occurs when SQL Server uses a nonclustered index to find rows that match a condition (like WHERE DepartmentID = 5), but still needs to fetch extra column data from the clustered index. It does this for every matching row.
To understand this better, think of a nonclustered index as a short-cut list. It can quickly tell SQL Server where rows are based on certain columns. But if your query needs additional data that’s not in that list, SQL Server has to go back to the original table (the clustered index) and fetch those missing columns, one row at a time. That’s the Key Lookup.
This process can become very expensive when your query returns many rows. Each lookup is an extra read, and those reads add up quickly.
Running a Demo to Trigger a Key Lookup
Let’s look at a practical example to see this in action.
SET STATISTICS IO ON; DROP TABLE IF EXISTS Employees; CREATE TABLE Employees ( EmployeeID INT IDENTITY PRIMARY KEY, FirstName NVARCHAR(50), LastName NVARCHAR(50), DepartmentID INT, Salary MONEY, HireDate DATE ); ;WITH Numbers AS ( SELECT TOP 100000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM master.dbo.spt_values a CROSS JOIN master.dbo.spt_values b ) INSERT INTO Employees (FirstName, LastName, DepartmentID, Salary, HireDate) SELECT 'First' + CAST(n AS NVARCHAR(10)), 'Last' + CAST(n AS NVARCHAR(10)), n % 1000 + 1, 60000 + (n % 10) * 1000, DATEADD(DAY, -n % 3650, GETDATE()) FROM Numbers; CREATE NONCLUSTERED INDEX idx_DeptOnly ON Employees(DepartmentID); UPDATE STATISTICS Employees; SELECT FirstName, LastName, Salary FROM Employees WHERE DepartmentID = 5;
When this query runs, SQL Server uses the idx_DeptOnly index to seek out rows where DepartmentID = 5. But it still needs to fetch FirstName, LastName, and Salary, which aren’t included in the index. So it performs a Key Lookup for each row.
Here’s what that looks like in the Actual Execution Plan:
This plan shows 100 Key Lookups—one for each row matching DepartmentID = 5.
You can also see the performance impact in the I/O statistics:
Table ‘Employees’. Scan count 1, logical reads 317, physical reads 0
Even though we’re only retrieving 100 rows, the system performs 317 logical reads. That means SQL Server had to read 317 data pages from memory to fulfill the query—most of those from repeated lookups.
What Is a Covering Index?
A covering index is an index that includes all the columns required by a query—either as key columns or as included columns. When SQL Server can find everything it needs in the index, it doesn’t have to go back to the base table at all. That means no Key Lookups.
In our case, if we include FirstName, LastName, and Salary in the index, SQL Server can serve the entire query from the index alone. That’s the fix.
Here’s how to create a covering index:
DROP INDEX idx_DeptOnly ON Employees; CREATE NONCLUSTERED INDEX idx_Dept_Covering ON Employees(DepartmentID) INCLUDE (FirstName, LastName, Salary); SELECT FirstName, LastName, Salary FROM Employees WHERE DepartmentID = 5;
Now, rerun the same query and examine the execution plan. The Key Lookup is gone. SQL Server now uses just one index seek to get everything in a single step. There’s no need to fetch anything from the clustered index.
Also check the updated I/O statistics. You should see a significant drop in logical reads. In many cases, the number drops from over 300 down to just a handful—typically 3 to 6 logical reads.
Table ‘Employees’. Scan count 1, logical reads 5, physical reads 0
This means SQL Server is doing far less work to retrieve the same data, and that translates into faster query response times and lower resource usage.
Why This Matters – Key Lookups
Key Lookups can quickly become a hidden performance tax on your queries. If your query is returning 1,000 rows, and each one triggers a Key Lookup, you’re adding 1,000 extra reads. That adds up quickly—especially in high-concurrency environments.
A covering index lets SQL Server skip the extra lookups and return everything in one go. It’s one of the simplest and most effective performance tuning techniques out there.
Final Thoughts
When you’re tuning queries in SQL Server, the Key Lookup should always be one of the first things you look for. It’s often invisible until you check the execution plan or measure logical reads. But once you catch it, the fix is usually straightforward: create a covering index with the INCLUDE clause.
This small change can lead to big improvements in performance—sometimes instantly.
Let me know if you’d like to see this as a downloadable demo file, or if you want a follow-up post on how to monitor Key Lookups across your entire database.
You can connect with me on LinkedIn or hire me for a Comprehensive Database Performance Health Check.
Reference: Pinal Dave (https://blog.sqlauthority.com)
3 Comments. Leave new
Good one sir. I recommend to a create cluster index if doesn’t exist. So that save the disk space.
Really informative article about something that can really cause performance degradation.
A follow-up post on how to monitor Key Lookups across your entire database would be great!
Thanks Pinal, for nice explanation. What if my Select has to return, say, 10 columns from the table. Is it okay to add lot of columns in [Include] for the non-clustered index? I have always been puzzled by this. Thanks,