Inspect composite-index column order, range boundaries, ICP, and covering lookup from real MySQL 8.4.10 plans and a 131,072-row fixture.
Misordered
- Selected key
idx_orders_status_date_customer
- Access type
range
- used_key_parts
status, ordered_at
Read the SQL and index definition together
Trace customer_id=412, status='PAID', the ordered_at lower bound, descending order, and LIMIT 20 through one access path.
Inspect the range access MySQL selected
Read the selected key, access type, key length, estimated rows, filtered, and cost from the same native range-scan operator.
Only status forms the leftmost equality prefix
The leading status='PAID' column forms the equality prefix; customer_id=412 appears after the ordered_at range and does not extend it.
status and ordered_at form the base range
used_key_parts and the normal range alternative contain status and ordered_at. These parts define the base candidate span across the date range.
customer_id participates as a boundary suffix and in ICP
The low-LIMIT recheck includes customer_id<=412 as a suffix in the chosen tuple, while ICP evaluates customer_id=412 during the scan. id preserves result order.
The status/date range contains 31,348 rows
M_RANGE is the exact set of 31,348 fixture rows satisfying status='PAID' and the ordered_at lower bound, not an engine visit trace.
customer_id=412 leaves 31 rows
Of 31,348 candidates, 31 have customer_id=412 and 31,317 are rejected by the ICP predicate in the fixture relation.
total_cents requires the base row
The selected secondary index lacks total_cents, so the model shows the logical clustered-row lookup through the index entry's primary key.
Index order satisfies ORDER BY
ordered_at DESC, id DESC matches the selected index order, so the plan has no separate filesort operator.
Stop at the twentieth tuple
Order 83356 is the last returned row and order 79260 is the next excluded row. Compare their ordered_at and id values directly.
Return the captured twenty rows
Verify that all three variants return the same twenty rows and checksum f3b6a116…01a8b, including IDs, timestamps, and totals.
Reconcile plan, trace, counters, and result
Read used_key_parts and ranges from plan/trace, candidate relations from the fixture, and rows, cost, Handler counters, and result from their capture locators.
Captured result
MySQL 8.4.10 · 20 rows · f3b6a116a0d644412cd60167a96e4e8d628adbe75106f53d78c3893fef401a8b
Aligned
- Selected key
idx_orders_customer_status_ordered_id
- Access type
range
- used_key_parts
customer_id, status, ordered_at
Read the SQL and index definition together
Trace customer_id=412, status='PAID', the ordered_at lower bound, descending order, and LIMIT 20 through one access path.
Inspect the range access MySQL selected
Read the selected key, access type, key length, estimated rows, filtered, and cost from the same native range-scan operator.
customer_id and status form the equality prefix
customer_id=412 and status='PAID' are consecutive leading equality conditions in the index.
ordered_at is the first boundary of a 31-row range
used_key_parts contains customer_id, status, and ordered_at; the ordered_at lower bound is the first range boundary after two equalities.
id remains an ordering suffix
id DESC does not further filter the range; it breaks ties for rows sharing the same ordered_at value.
The aligned prefix narrows the range to 31 rows
A_RANGE contains exactly 31 rows satisfying customer_id=412, status='PAID', and the ordered_at bound, equal to ELIGIBLE.
All 31 range candidates pass
customer_id and status are already in the range prefix, leaving no rejected fixture rows among the 31 candidates.
The range is narrow, but total_cents is still in the base row
This index still lacks total_cents, so projecting the 31 candidates requires the clustered-row materialization model.
Index order satisfies ORDER BY
ordered_at DESC, id DESC matches the selected index order, so the plan has no separate filesort operator.
Stop at the twentieth tuple
Order 83356 is the last returned row and order 79260 is the next excluded row. Compare their ordered_at and id values directly.
Return the captured twenty rows
Verify that all three variants return the same twenty rows and checksum f3b6a116…01a8b, including IDs, timestamps, and totals.
Reconcile plan, trace, counters, and result
Read used_key_parts and ranges from plan/trace, candidate relations from the fixture, and rows, cost, Handler counters, and result from their capture locators.
Captured result
MySQL 8.4.10 · 20 rows · f3b6a116a0d644412cd60167a96e4e8d628adbe75106f53d78c3893fef401a8b
Covering
- Selected key
idx_orders_customer_status_ordered_id_total
- Access type
range
- used_key_parts
customer_id, status, ordered_at
Read the SQL and index definition together
Trace customer_id=412, status='PAID', the ordered_at lower bound, descending order, and LIMIT 20 through one access path.
Inspect the range access MySQL selected
Read the selected key, access type, key length, estimated rows, filtered, and cost from the same native range-scan operator.
customer_id and status form the same equality prefix
As in the noncovering variant, customer_id=412 and status='PAID' fix the leading equality prefix.
ordered_at forms the same 31-row range
Adding the covering suffix leaves used_key_parts and the first range boundary unchanged: customer_id, status, ordered_at.
id orders rows and total_cents completes projection
id DESC is the ordering suffix, while total_cents is the suffix that satisfies the SELECT projection inside the index entry.
Covering does not change the 31-row range
The total_cents suffix does not narrow the range, so A_RANGE remains the same 31 rows as in the noncovering variant.
All 31 rows remain result candidates
The leading equalities and range cover every WHERE predicate, producing 31 passes and zero rejects in the fixture.
The index entry supplies every selected value
With total_cents in the secondary index, all selected columns are projected without a base-row materialization model.
Index order satisfies ORDER BY
ordered_at DESC, id DESC matches the selected index order, so the plan has no separate filesort operator.
Stop at the twentieth tuple
Order 83356 is the last returned row and order 79260 is the next excluded row. Compare their ordered_at and id values directly.
Return the captured twenty rows
Verify that all three variants return the same twenty rows and checksum f3b6a116…01a8b, including IDs, timestamps, and totals.
Reconcile plan, trace, counters, and result
Read used_key_parts and ranges from plan/trace, candidate relations from the fixture, and rows, cost, Handler counters, and result from their capture locators.
Captured result
MySQL 8.4.10 · 20 rows · f3b6a116a0d644412cd60167a96e4e8d628adbe75106f53d78c3893fef401a8b