Covering Indexes Explained Independently of Any Framework
AI generated
SELECT
JOIN
SQL / Indexing
Covering Indexes Explained Independently of Any Framework
how an index makes the table lookup entirely unnecessary

A regular index speeds up finding rows based on a filter condition, but in most cases still requires an additional access to the actual table afterward to fetch the remaining requested columns. This extra jump, often called a key lookup or bookmark lookup, costs noticeable time with many matches. A covering index already contains every column a query needs, making the lookup entirely unnecessary, an effect known as an index-only scan.

10 min read Covering Index Index-Only Scan

1. How a regular index works and why a lookup becomes necessary

A classic secondary index stores the indexed column values together with a reference to the complete row, either directly as a physical row address or, with a clustered primary key, as the value of that primary key. When the database finds matching entries via the index, the index itself initially contains only the indexed columns and the reference, not the other columns requested by the query.

If additional columns are needed that are not part of the index, the database has to additionally fetch the complete row from the table for every match found. This second access is called a key lookup or bookmark lookup and, with an index residing on a different physical structure than the table itself, means another, often random, I/O access per match.

2. Why lookup costs add up considerably with many matches

For a query with few matches, an additional lookup per row barely matters, because the absolute number of extra accesses stays small. For a query that finds thousands or millions of rows via the index and performs a separate lookup for each of them, these many small, often randomly scattered accesses across the disk add up to considerable overall load that partly eats into the theoretical benefit of the index.

This is exactly where the optimizer's cost estimate kicks in: past a certain estimated match count, a full table scan can appear cheaper than an index scan with many individual lookups despite an available index, because a sequential scan is more I/O-efficient than many scattered individual accesses. This behavior is often mistakenly interpreted as proof that the index is broken or unsuitable, even though it is a rational cost decision.

3. The core principle of a covering index

A covering index solves this problem by additionally containing, alongside the actual filter and sort columns, every column a given query needs beyond that. When the database finds matching entries in the index, every requested value is already available in the index itself, so no additional access to the table is needed at all. This pattern is called an index-only scan and is usually noticeably faster than an index scan followed by individual lookups.

The important distinction here is between key columns, which determine the sort order of the index and are used for range filters or sorting, and additional, non-key columns that are merely carried along to avoid lookups, without themselves influencing sort order. Many database systems offer an explicit INCLUDE clause for this.


-- Query requiring a lookup with only a partially matching index
CREATE INDEX idx_orders_customer ON orders (customer_id);

SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 4711;
-- The index finds the row, order_date/total_amount need
-- to be fetched via a lookup from the table

-- Covering index: contains every needed column
CREATE INDEX idx_orders_customer_covering
ON orders (customer_id) INCLUDE (order_date, total_amount);
-- The same query is now executed as an index-only scan

4. Practical example: spotting an index-only scan in the execution plan

The difference shows up clearly in the execution plan: without a covering index, a separate step for table access appears alongside the index access, often explicitly labeled a key lookup or bookmark lookup, connected via a nested-loop join with the index access. With a suitable covering index, this second step disappears entirely, and the plan shows only a single access, frequently labeled explicitly as an index-only scan or covering-index scan.

When analyzing an existing, slow execution plan, it therefore pays to look specifically for whether an additional lookup step follows an index seek or index scan. If that is the case and the lookup makes up a considerable share of total cost, extending the index with the missing columns is usually the most obvious and effective optimization.


EXPLAIN
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = 4711;

-- Without a covering index (excerpt):
--   -> Index Lookup on orders using idx_orders_customer
--   -> Key Lookup on orders (fetches order_date, total_amount)

-- With a covering index (excerpt):
--   -> Covering Index Scan on idx_orders_customer_covering
--      (no additional table-access step remains)

5. The trade-off: larger indexes against fewer lookups

Every additional column in a covering index physically enlarges that index, tying up more disk space and more memory in the buffer pool or page cache. A larger index also means more data that has to be additionally written and inserted in sorted order into the index tree on every insert, update, or delete affecting the indexed or included columns, which raises write load.

This trade-off is not a blanket argument against covering indexes, but a balancing act that should be judged per query pattern: for a very frequently executed, read-heavy query with many matches, the lookup savings usually clearly justify the extra storage and write overhead. For a rarely executed query with few matches, the extra storage is often not worth it.

6. INCLUDE columns vs. additional key columns

A column carried along via INCLUDE differs technically from an additional key column, because it is not part of the sorted tree structure of the index, but is only stored at the leaf nodes. That reduces overhead compared to a full-fledged additional key column, because neither the tree height nor the sort logic is affected by the extra column, but at the same time an INCLUDE column cannot be used for range filters or sorting.

Systems without an explicit INCLUDE clause achieve the same effect by simply adding the needed columns as additional, lower-priority key columns to the index. The effect on the index-only scan is functionally similar, but the write overhead tends to be somewhat higher, because these columns then also influence sort order and thus the structure of the tree.

7. Why SELECT * almost always defeats a covering index

A covering index can only take effect if the set of requested columns is actually known and bounded. A query with SELECT * implicitly requests every column of the table, which would require a covering index to contain every column of the table to be effective, negating its original size advantage over the table itself. In practice, SELECT * therefore almost always means the optimizer falls back to a classic lookup or a full table scan.

Explicitly listing the actually needed columns is therefore not just generally good style, but a direct prerequisite for a covering index to be effective at all. This discipline in column selection is a simple but often underrated lever, especially for frequently executed, performance-critical queries.

8. Maintenance overhead: keeping covering indexes aligned with changing queries

A covering index is tightly bound to a specific query pattern. If the application changes and a previously frequently used query suddenly requests an additional column not contained in the index, the index-only scan falls back to a lookup again, without this being immediately noticeable unless execution plans are reviewed regularly. Covering indexes therefore need more ongoing maintenance than generic indexes on individual filter columns.

In practice, it works well to create covering indexes deliberately for a small number of particularly frequent, performance-critical queries and to review regularly, for instance after larger application changes, whether they still apply. For rarely used or constantly changing queries, on the other hand, the maintenance effort of a tailored covering index rarely pays off.

9. A practical decision rule for when to use one

A covering index is most worthwhile for queries that are executed frequently, need a manageable, stable set of columns, and currently show a noticeable lookup share of total cost in the execution plan. If any of these three factors is missing, especially a high execution frequency, the extra storage and maintenance effort is usually not justified.

Before making a decision, it pays to look at the current execution plan of the affected query with real production data, because only there does it become visible how large the lookup share actually is. An optimization without this evidence remains speculative and can, in the worst case, increase write load without measurably speeding up the read path.

Aspect Regular index Covering index Consequence
Columns contained Only filter/sort columns Additionally all requested columns No lookup needed with covering
Additional table access Key lookup per match Eliminated entirely Index-only scan possible
Index size Smaller Larger due to INCLUDE columns More storage and cache demand
Write load Lower Somewhat higher More data written on every insert/update
SELECT * Works, but with a lookup Almost always prevents covering Explicit column list required
Maintenance overhead Low, generically usable Tightly bound to query pattern Regular plan review required

Mironsoft

Database optimization, query tuning, and migrations

SQL queries that keep getting slower as the data grows?

We analyze and optimize SQL databases regardless of the system in use, plan safe migrations and schema changes, and teach teams query optimization hands-on.

Query Optimization

Analyze slow queries and speed them up with purpose using indexes and explain plans.

Migration Planning

Execute schema changes and data migrations safely, without downtime.

Team Training

Anchor SQL fundamentals and performance thinking hands-on in the dev team.

10. Summary

Covering Indexes: Key Facts at a Glance

Core idea

A covering index contains every column a query needs, making an additional table lookup unnecessary.

Effect

The execution plan shows an index-only scan instead of an index access plus a separate key lookup per match.

Trade-off

Larger, more write-expensive indexes are traded against fewer lookups and faster read queries.

Prerequisite

Only an explicit, bounded column list instead of SELECT * can actually benefit from a covering index.

11. FAQ: Covering Indexes: Key Facts at a Glance

1What is the difference between a regular index and a covering index?
A regular index contains only the filter and sort columns plus a reference to the row, a covering index additionally contains every other column requested by the query, so no extra access to the table is needed.
2What is a key lookup or bookmark lookup?
The additional access to the complete table row that the database performs when an index does not contain every requested column. With many matches, this access adds up to considerable overhead.
3What does index-only scan mean?
An execution step where all needed data is read directly from the index, without touching the actual table at all. It is only possible with a suitable covering index.
4Why does SELECT * almost always defeat a covering index?
Because SELECT * implicitly requests every column of the table. An index would then need to contain every column to cover effectively, which negates its size advantage over the table itself.
5What is the difference between an INCLUDE column and an additional key column?
An INCLUDE column is only stored at the leaf nodes of the index and does not affect sort order, an additional key column is part of the sorted tree structure and can be used for range filters.
6Is a covering index worthwhile for every query?
No. It pays off mainly for frequently executed queries with a stable column set and a noticeable lookup share in the execution plan. For rare queries, the extra storage and maintenance effort usually outweighs the benefit.
7How do I spot in the execution plan whether a lookup happens?
An additional step following the index access, often labeled key lookup or bookmark lookup, indicates a missing covering index. If that step is absent, an index-only scan is already in effect.
8Does a covering index increase write load?
Yes, because additional columns have to be written along on every insert or update. With INCLUDE columns, this overhead is lower than with full additional key columns.
9Does a covering index need regular review?
Yes, because it is tightly bound to a specific query pattern. If the query changes and requests an additional column not included, the index-only scan falls back to a lookup again.
10Is a covering index implemented the same way across databases?
The core principle is identical, but the concrete syntax differs. Some systems offer an explicit INCLUDE clause, others achieve the same effect through additional, lower-priority key columns.