Q1Why does an Index-Only Scan avoid going back to the base table?
Index-Only Scan — An Index That Skips the Table
This article is part of the SQL Course, where you master practical SQL skills from scratch, from the fundamentals through to complex queries and SQL tuning.
Put every column the query touches into the index and the trip back to the base table disappears. This is an Index-Only Scan (also known as a covering index). You'll learn the conditions that make it work, how it breaks the moment even one column is missing, and how to fold the WHERE filter columns into a single index, all verified with EXPLAIN QUERY PLAN.
Skip the table lookup — the Index-Only Scan
A regular index lookup finds the target row in the index, then goes back to the base table to read the other columns one row at a time.
This trip back is called a table lookup (the step of pulling a row from the base table via the index).
When the result set is large, those round trips add up and start to cost real time.
If you use an index that contains every column the query references, all the values are right there in the index, and there's no need to go back to the table itself.
This pattern, where the index alone delivers the result, is called an Index-Only Scan (an index that covers every column the query touches is also known as a covering index).
-- Aggregating region with an index that contains only region
-- The referenced column (region) is fully inside the index → no trip back to the base table
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(region);
EXPLAIN QUERY PLAN
SELECT region, COUNT(*)
FROM perf_sales
GROUP BY region;
Miss even one column and you go back to the base table
An Index-Only Scan only happens when every column the query touches — in SELECT, WHERE, GROUP BY, and so on — is inside the index.
Miss even one column and the database has to go back to the base table to read it, and USING COVERING INDEX disappears from the plan.
For example, with an index on (emp_id, amount), writing SELECT emp_id, SUM(amount), region adds region, which isn't in the index — so the query heads back to the base table to fetch it.
-- With an index on (region, amount),
-- adding product to the referenced columns breaks the Index-Only Scan
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(region, amount);
-- region, SUM(amount) only → fully covered by the index
EXPLAIN QUERY PLAN
SELECT region, SUM(amount) FROM perf_sales GROUP BY region;
-- Add product → not in the index, so back to the base table
EXPLAIN QUERY PLAN
SELECT region, SUM(amount), MAX(product) FROM perf_sales GROUP BY region;
Keep the Index-Only Scan by including WHERE filter columns too
SELECT isn't the only place columns show up in your query.
WHERE filter columns also count as columns the query touches, so they need to be in the index too — otherwise you still end up going back to the base table.
Fold them all into one index in the order "filter columns → output / aggregate columns", and both the filtering and the value retrieval finish inside the same index.
For example, to filter with WHERE region = 'East' and compute SUM(amount), put the filter column region first and the aggregate column amount second: (region, amount).
The index narrows down to the target rows by region and reads amount from the same index, so no trip back to the base table is needed.
-- Fold a filter column (status) and an aggregate column (amount) into one index
DROP INDEX IF EXISTS ix_demo;
CREATE INDEX ix_demo ON perf_sales(status, amount);
EXPLAIN QUERY PLAN
SELECT SUM(amount)
FROM perf_sales
WHERE status = 'pending';
Knowledge Check
Answer each question one by one.
Q2Given an index on (emp_id, amount), which query breaks the Index-Only Scan and goes back to the base table?
Q3For a query that filters with WHERE region = 'East' and computes SUM(amount), which index supports an Index-Only Scan?