MySQL Covering Index & Back‑Table Optimization: Hands‑On Guide to Eliminate Lookups
The article explains why MySQL back‑table lookups (回表) double I/O and degrade performance, defines covering indexes that retrieve all needed columns from a secondary index, shows how to identify back‑table cases via EXPLAIN, and provides step‑by‑step commands to create, test, and verify covering indexes for high‑frequency queries.
What: Back‑Table Lookup & Covering Index Basics
In InnoDB secondary indexes only store the indexed columns and the primary‑key ID, not the full row. When a query requests columns outside the index, MySQL first reads the secondary index to get the primary key, then accesses the clustered primary‑key index to fetch the full row. This extra index I/O is called a back‑table lookup.
A covering index contains every column required by the query, so the secondary index alone can satisfy the request without touching the clustered index, completing the query with a single I/O operation.
One‑sentence summary: Back‑table = two index reads (slow); covering index = one index read (fast).
Why: Back‑Table Lookups Are Hidden Performance Killers
Many beginners assume that using any index is optimal, which is a major misconception. In production, slow queries, high‑concurrency jitter, and high DB I/O often stem from frequent back‑table lookups despite index usage.
Each query incurs double I/O (secondary + primary), roughly doubling latency.
In high‑concurrency scenarios, millions of requests amplify I/O pressure, causing DB load spikes.
Pagination and list queries generate many back‑table operations, easily hitting slow‑query thresholds.
Endpoints that could respond in ~5 ms may stretch to 30‑100 ms, severely limiting performance.
Covering indexes push a single‑SQL statement to the database’s performance limit without sacrificing business logic or adding caches, making them the optimal MySQL index‑optimization technique.
Where: Suitable & Unsuitable Scenarios
When to use covering indexes
High‑frequency list or pagination queries (order list, user list, record list).
High‑concurrency simple‑query APIs (high QPS, few return columns).
Statistical or count queries that retrieve only a few columns.
SQL statements flagged as slow despite using an index.
When not to force a covering index
Queries that select many columns or use SELECT * (full row retrieval).
Forcing a covering index in such cases inflates index size to near table size, exploding write‑update costs.
Low‑frequency or one‑off backend queries where optimization is unnecessary.
Columns that are frequently updated; adding them to a covering index harms write performance.
Core Identification: Spotting Back‑Table Lookups
Inspect the EXPLAIN output’s Extra column: Using index → covering index hit, no back‑table, optimal performance. Extra empty or contains Using where → index used but back‑table occurs, optimization possible. Using filesort or Using temporary → back‑table plus sorting/temporary table, very poor performance.
How: Hands‑On Step‑by‑Step Demo
Using a realistic user_order table, the article reproduces the back‑table phenomenon and then eliminates it with a covering index.
Step 1 – Create test table and data
CREATE TABLE `user_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'Primary Key ID',
`order_no` varchar(32) NOT NULL COMMENT 'Order Number',
`user_id` bigint NOT NULL COMMENT 'User ID',
`status` tinyint NOT NULL DEFAULT '0' COMMENT 'Order Status',
`pay_time` datetime DEFAULT NULL COMMENT 'Payment Time',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Order Test Table';Step 2 – Create a regular single‑column index (produces back‑table)
CREATE INDEX idx_user_id ON user_order(user_id);Step 3 – Reproduce back‑table query
Scenario: fetch order_no and status by user_id.
EXPLAIN SELECT order_no, status FROM user_order WHERE user_id = 1001;Result: type = ref (index hit). Extra does not contain Using index.
Back‑table lookup is inevitable.
Execution flow:
Use idx_user_id to locate primary‑key IDs for user_id = 1001.
With each primary key, read the clustered index to retrieve order_no and status.
Two index I/O operations cause significant performance waste.
Step 4 – Create a covering index to eliminate back‑table
Covering‑index formula: condition columns first, then columns needed in the SELECT list.
DROP INDEX idx_user_id ON user_order;
CREATE INDEX idx_user_cover ON user_order(user_id, order_no, status);Step 5 – Verify optimization
EXPLAIN SELECT order_no, status FROM user_order WHERE user_id = 1001;Result: Extra now shows Using index → full covering index hit.
No back‑table, no primary‑key access.
Single‑index I/O completes the query, maximizing performance.
Enterprise‑Level Covering Index Design Guidelines
Universal index order: WHERE condition fields → ORDER BY fields → SELECT return fields .
Example business query:
SELECT order_no, status, pay_time
FROM user_order
WHERE user_id = 1001 AND status = 1
ORDER BY create_time DESC;Optimal covering index:
CREATE INDEX idx_full_cover ON user_order(user_id, status, create_time, order_no, pay_time);This single index simultaneously addresses index hit, sorting, and back‑table elimination.
Common Pitfalls in Production
Myth: Using an index always means optimal performance – false; back‑table can still cause heavy loss.
SELECT * prevents covering index hits and forces back‑table lookups.
Over‑designing covering indexes inflates index size, causing write‑performance collapse.
Incorrect index column order renders the index ineffective, nullifying covering benefits.
Ignoring primary‑key natural covering – secondary indexes already include the primary key, so a simple primary‑key lookup may already be covering.
Quick Reference Cheat Sheet (Interview & Production)
Back‑table essence: secondary index → primary key → clustered index = double I/O.
Covering index essence: all query columns reside in the secondary index = single I/O, maximal speed.
Identification rule: EXPLAIN Extra = Using index indicates a covering index.
Index creation rule: condition columns first, then sorting columns, then return columns.
Optimization baseline: few columns, high‑frequency queries → enforce covering; many columns, low‑frequency → avoid covering.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
liandk
Seasoned Java and mobile developer with years of experience, specializing in mini‑programs, public accounts, and full‑stack front‑end development. In the AI era, I continuously learn to broaden my knowledge and evolve. I revived a public account I started a decade ago during a dessert‑startup venture, using code as a vessel and knowledge as a companion. I share personal projects, technical articles, programming tips, and growth insights—let’s improve together and set sail.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
