SQL Server Index Syntax: Covering Indexes, INCLUDE Columns, and Filtered Indexes

Written by

in

SQL Server Index Syntax: Covering Indexes, INCLUDE Columns, and Filtered Indexes

Beyond a basic index, two techniques do most of the real performance work: covering indexes, which eliminate the key lookup from the previous lesson entirely, and filtered indexes, which shrink an index down to just the rows that actually matter.

Covering & filtered indexes(what actually lives in the index)one index row:KEY:customer_idsets sort order+INCLUDE:order_status, order_totaljust riding along, no sortcovers the whole queryno Key Lookup!Key Lookupfiltered index = only the rows that match WHERE:shippedshippedpendingcompletedpendingshippedIX_OrderLog_PendingOnlytiny index — only ‘pending’ rowsoptimizer only picksthis index when WHEREclause PROVES a match —otherwise: full scan 📌

Covering Index with INCLUDE

CREATE NONCLUSTERED INDEX IX_OrderLog_Customer_Covering
ON dbo.OrderLog (customer_id)
INCLUDE (order_status, order_total);

-- Fully satisfied by the index — no key lookup needed
SELECT customer_id, order_status, order_total
FROM dbo.OrderLog
WHERE customer_id = 42;

“Covering” means every column the query needs — for filtering, sorting, or just selecting — exists somewhere in the index itself, so the engine never has to jump back to the clustered index at all. This directly eliminates the exact key-lookup cost the previous lesson demonstrated.

Key Columns vs INCLUDE Columns

Key columns Determine sort order Usable for seeking/filtering INCLUDE columns Just ride along at the leaf Avoid a lookup, can’t be used to seek

Put columns you filter/sort on in the key; put columns you only ever SELECT in INCLUDE — this keeps the index narrower and cheaper to maintain than making everything a key column. Key columns also enforce sort order (relevant to ORDER BY), while INCLUDE columns carry no ordering guarantee at all — they’re purely along for the ride.

Common mistake: Putting every SELECTed column into the key list “to be safe.” Wider key columns mean a physically larger B-tree, more page splits on insert, and more expensive maintenance on every write — INCLUDE exists specifically to avoid that cost for columns that only need to be read, never searched or sorted on.

Filtered Index: Indexing Just a Subset

CREATE NONCLUSTERED INDEX IX_OrderLog_PendingOnly
ON dbo.OrderLog (order_date)
WHERE order_status = 'pending';

Ideal when queries consistently target a small, well-defined subset of a large table — the index is smaller, faster to scan, and cheaper to maintain since it only updates when a matching row changes (a row with order_status = 'completed' never touches this index at all, on insert or update).

-- The optimizer only uses a filtered index when the query's WHERE clause
-- provably matches (or is a subset of) the index's filter condition:
SELECT * FROM dbo.OrderLog WHERE order_status = 'pending' AND order_date > '2026-01-01';
-- Uses IX_OrderLog_PendingOnly — the query's filter is compatible with the index's

SELECT * FROM dbo.OrderLog WHERE order_date > '2026-01-01';
-- Does NOT use it — this query has no order_status filter, so the index can't
-- guarantee it covers every matching row
Practice tip: Build both indexes above, then compare sys.dm_db_index_physical_stats page counts between the filtered index and an unfiltered equivalent covering the same key column, on a table where ‘pending’ is a small fraction of total rows. The size difference makes the benefit concrete rather than theoretical.

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.