Chapter 08 · Index Engineering and Access Path Design

Index Condition Pushdown, Loose/Tight Index Scans, and Skip-Scan Concepts

Read optimizer evidence for advanced index access paths, understand why ICP can avoid base-row reads, and recognize loose, tight, and skip-scan choices as cost-based optimizations with prerequisites.

Beginner → Intermediate125–155 minoptimizer access-path labMySQL Community Server 8.4.10 LTS · InnoDB · free local labICP + loose/tight/skip scanLast reviewed: August 2026

Learning outcomes

Sometimes a query becomes faster without a new index because MySQL finds a more efficient way to use the index you already have. That does not make these access methods tuning switches to force everywhere. They are optimizer strategies selected when their structural prerequisites and estimated cost make sense.

01

Explain the server/storage-engine boundary that Index Condition Pushdown changes for InnoDB secondary-index scans.

02

Recognize EXPLAIN evidence such as Using index condition, Using index for group-by, and Using index for skip scan.

03

Distinguish Loose Index Scan from Tight Index Scan for eligible GROUP BY/DISTINCT patterns.

04

Explain skip scan as multiple range scans across missing leading-key values rather than a repeal of B-tree ordering.

05

Run controlled optimizer-switch experiments without turning FORCE INDEX or hints into production folklore.

Index Condition Pushdown: reject from the secondary entry first

Create an index where the first two parts form a useful range and a later indexed column can reject candidates before InnoDB fetches the full clustered row:

sql · prepare an ICP-friendly secondary index
USE servicehub_index_lab;CREATE INDEX ix_wo_icp  ON work_orders (tenant_id, opened_at, status);ANALYZE TABLE work_orders;EXPLAINSELECT work_order_id,customer_name,detailsFROM work_ordersWHERE tenant_id=17  AND opened_at BETWEEN '2026-02-01' AND '2026-05-31'  AND status='open';

The range can use tenant_id and opened_at. The later status value is present in the secondary index entry. With Index Condition Pushdown (ICP), MySQL can push the index-checkable condition into the storage engine so entries failing status='open' can be rejected before full rows are fetched for customer_name and details. Traditional EXPLAIN reports Using index condition when ICP is used.

sql · controlled ICP comparison—not a production recipe
SET @saved_optimizer_switch = @@SESSION.optimizer_switch;SET SESSION optimizer_switch='index_condition_pushdown=off';EXPLAIN ANALYZESELECT work_order_id,customer_name,details FROM work_ordersWHERE tenant_id=17 AND opened_at BETWEEN '2026-02-01' AND '2026-05-31' AND status='open';SET SESSION optimizer_switch='index_condition_pushdown=on';EXPLAIN ANALYZESELECT work_order_id,customer_name,details FROM work_ordersWHERE tenant_id=17 AND opened_at BETWEEN '2026-02-01' AND '2026-05-31' AND status='open';SET SESSION optimizer_switch=@saved_optimizer_switch;

Record actual rows and local timing, but do not expect a dramatic difference on 20,000 warm rows. ICP’s mechanism is reducing unnecessary full-row reads. MySQL 8.4 documents ICP for range, ref, eq_ref, and ref_or_null access where full rows are needed; for InnoDB, ICP applies to secondary indexes, not the clustered index.

Loose Index Scan: jump between groups

A Loose Index Scan can exploit ordered keys so MySQL does not read every key in every group. Its eligibility is strict. A classic case groups by a leftmost prefix and asks for MIN()/MAX() of the immediately following index part.

sql · create an index suitable for a loose grouping experiment
CREATE INDEX ix_wo_tenant_opened  ON work_orders (tenant_id,opened_at);ANALYZE TABLE work_orders;EXPLAINSELECT tenant_id, MIN(opened_at), MAX(opened_at)FROM work_ordersGROUP BY tenant_id;

If selected, traditional EXPLAIN reports Using index for group-by. The important reasoning is why it is eligible: one table, group columns form a leftmost index prefix, and the MIN/MAX column follows that prefix. Change the query to SUM(priority) and this particular Loose Index Scan rule no longer applies.

sql · break the loose-scan precondition deliberately
EXPLAINSELECT tenant_id, SUM(priority)FROM work_ordersGROUP BY tenant_id;

Tight Index Scan: fill key gaps, then group

A Tight Index Scan may still use an ordered index for grouping even when the looser group-jumping strategy does not apply. It reads all keys in the qualifying range and groups after the scan. Equality predicates can fill leading key parts that would otherwise be gaps.

sql · tight-scan-style grouping shape
EXPLAINSELECT status, COUNT(*)FROM work_ordersWHERE tenant_id=17GROUP BY status;

The dashboard index begins with (tenant_id,status,...). Fixing tenant_id=17 supplies the leading constant, so rows for status are encountered in index order. MySQL does not expose a literal “Tight Index Scan” label in the same way it exposes Using index for group-by for Loose Index Scan; reason from the key, range, and whether a temporary/sort step is needed.

Skip scan: use later key parts by iterating leading values

Ordinary leftmost-prefix rules still hold. Skip scan is a special range access method that can make a composite index useful when the leading key part lacks a query predicate but has relatively few distinct values. Conceptually, the optimizer performs multiple ranges—one per relevant leading value—and applies a range on a later key part.

sql · build a low-cardinality-leading-key skip-scan candidate
CREATE INDEX ix_wo_status_opened  ON work_orders(status,opened_at,work_order_id);ANALYZE TABLE work_orders;SHOW VARIABLES LIKE 'optimizer_switch';EXPLAINSELECT status,opened_at,work_order_idFROM work_ordersWHERE opened_at >= '2026-06-15'  AND opened_at <  '2026-07-01';

If MySQL chooses skip scan, traditional EXPLAIN shows Using index for skip scan. It may instead choose a full/index scan because skip scan is cost-based. The lesson is not to force it until the label appears; the lesson is that a small number of leading-key values can make multiple later-key ranges cheaper than scanning everything.

sql · inspect the switch, then leave it at the saved setting
SET @saved_optimizer_switch2 = @@SESSION.optimizer_switch;SET SESSION optimizer_switch='skip_scan=off';EXPLAIN SELECT status,opened_at,work_order_id FROM work_ordersWHERE opened_at >= '2026-06-15' AND opened_at < '2026-07-01';SET SESSION optimizer_switch=@saved_optimizer_switch2;

Use that OFF experiment to understand whether skip scan changes the candidate plan on your data. Do not leave optimizer switches changed globally just to preserve a lab plan.

Tempting but ineffective: FORCE INDEX until EXPLAIN looks “indexed”

FORCE INDEX can be useful for diagnosis, but forcing an index because type=ALL looks visually bad can increase work. A table scan can be rational when a predicate returns most rows; a forced index can add random clustered lookups on top of scanning many secondary entries.

sql · compare optimizer choice with a forced candidate
EXPLAIN ANALYZE SELECT work_order_id,details FROM work_orders WHERE status IN ('open','assigned','waiting','closed');EXPLAIN ANALYZE SELECT work_order_id,details FROM work_orders FORCE INDEX(ix_wo_status_opened) WHERE status IN ('open','assigned','waiting','closed');

Record the actual iterator work. If the forced plan is worse, you have disproved the slogan “an index scan is always better.” If it is better on your machine, that is still only one local observation; Chapter 09 will address statistics and plan regressions more systematically.

Evidence checklist

EXPLAIN evidenceMechanism to investigateDo not overclaim
Using index conditionICP evaluated an index-checkable condition before full-row fetch.It does not mean the query is index-only; full rows can still be read.
Using index for group-byEligible loose grouping/distinct access.Not every GROUP BY on indexed columns qualifies.
Using index for skip scanOptimizer iterated ranges over missing leading values.Skip scan is cost-based and does not erase leftmost ordering.
Using indexIndex-only/covering access for the required values.It does not by itself prove low latency or good total workload cost.

Knowledge check

  1. What problem does ICP solve?
  2. What is the core idea of Loose Index Scan?
  3. How does Tight Index Scan differ conceptually?
  4. Does skip scan invalidate the leftmost-prefix rule?
  5. Why is FORCE INDEX a poor default tuning strategy?
Reveal answers
  1. It can test conditions from a secondary index entry before fetching the full InnoDB row, reducing unnecessary base-row reads.
  2. Use ordered index keys to jump between qualifying groups and read only a fraction of entries under strict query/index conditions.
  3. It reads all keys in the qualifying index range (or index) and groups them, often using equality constants to fill key-prefix gaps.
  4. No. It is a separate cost-based access method that effectively performs multiple ranges for leading key values so a later range can be exploited.
  5. It overrides cost-based choice and can force more work; it should be a diagnostic/exception tool backed by evidence, not a cosmetic fix for EXPLAIN.

Production judgment and bridge

Optimizer features are not trophies to maximize. ICP, loose/tight scans, and skip scan are useful when they reduce work for real query shapes. Keep default cost-based behavior unless repeated representative measurements justify an exception, and always restore session experiments.

Lesson 5 combines everything into an index portfolio: which indexes are worth keeping for the workload as a whole, which overlap, which only look useful in synthetic tests, and how to account for write/storage cost before removing or adding anything.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.