覆盖索引包含某条查询求值和返回结果所需的列。在 InnoDB 中,二级索引记录还会携带主键列,因此即使二级索引定义没有再次列出主键,某条查询也可能已经被覆盖。覆盖描述的是一条具体查询与某个索引之间的关系,不是给索引贴上标签之后,整张表的所有查询都自动获得同样行为。
坏掉的样子
增加返回列可能使覆盖失效。原本能提供标识和时间戳的索引,不一定包含应用后来开始读取的大载荷。优化器仍然可能沿用这个索引定位匹配记录,再访问聚簇记录取得缺失字段。因此,“用到了索引”和“由索引覆盖查询”是两件不同的事,不能从计划中出现索引名字就直接推导后者。
覆盖也不保证访问范围足够小。一个扫描大量条目的查询,可以完全被索引覆盖,仍然花费很多工作。列顺序、过滤条件、排序要求和行数估计都很重要。不要把 Using index 覆盖标记当作查询很快的证明,也不要把它与 Using index condition 索引条件下推标记混淆;后者描述部分条件在索引层求值,并不证明整个返回投影都在索引里。
InnoDB 的可见性检查还会限制通常所说的“只读索引”解释。二级记录不包含与聚簇记录相同的隐藏事务元数据。遇到删除标记条目,或被更新事务修改过的二级索引页时,可能仍需访问聚簇记录才能获得正确可见版本。因此,在并发多版本活动下,一个覆盖计划并不普遍保证聚簇访问次数永远为零。
快速验证
历史示例保持原样,当前没有 MySQL 8.4,因此本次没有重跑;计划结果仍属于既有运行记录。
准备一台新的 MySQL 8.4 实例,使用能够创建测试模式的客户端会话。既有运行记录中的版本是 MySQL 8.4.11。确认实验模式名尚未使用,遇到非预期 SQL 错误就停止,并保持同一个会话,以保留当前选中的数据库。下面建立一万行数据,其中一百笔已付款订单,并让载荷列明显比索引所需字段更宽:
CREATE DATABASE xbcatm_index_lab;
USE xbcatm_index_lab;
CREATE TABLE orders (
id INT PRIMARY KEY,
status VARCHAR(10) NOT NULL,
created_at DATETIME NOT NULL,
payload VARCHAR(1000) NOT NULL,
INDEX by_status_created (status,created_at)
) ENGINE=InnoDB;
INSERT INTO orders
WITH digits(n) AS (
SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
)
SELECT number+1,IF(MOD(number+1,100)=0,'paid','open'),
TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (number+1) SECOND,
REPEAT('x',640)
FROM (SELECT a.n+10*b.n+100*c.n+1000*d.n AS number
FROM digits a CROSS JOIN digits b CROSS JOIN digits c CROSS JOIN digits d) AS numbered;
ANALYZE TABLE orders;
SELECT COUNT(*),SUM(status='paid') FROM orders;
最后的两个计数应为一万与一百。二级索引明确包含状态与创建时间,InnoDB 隐式提供主键标识。先检查并运行只需要这些值的查询:
EXPLAIN SELECT id,created_at FROM orders WHERE status='paid' ORDER BY created_at,id LIMIT 20;
EXPLAIN ANALYZE SELECT id,created_at FROM orders WHERE status='paid' ORDER BY created_at,id LIMIT 20;
SELECT id,created_at FROM orders WHERE status='paid' ORDER BY created_at,id LIMIT 20;
既有表格计划给出所建索引及覆盖访问标记,实际分析计划显示覆盖索引查找并返回二十行。结果标识依次为一百、二百,直到两千,按照时间戳与标识排序。这是在已经填充数据的表上核对过的实际结果,不是从一个空模式推测查询会采用什么访问路径。
接着,把载荷也加入返回列:
EXPLAIN SELECT id,created_at,payload FROM orders WHERE status='paid' ORDER BY created_at,id LIMIT 20;
EXPLAIN ANALYZE SELECT id,created_at,payload FROM orders WHERE status='paid' ORDER BY created_at,id LIMIT 20;
既有计划仍使用同一个索引,却不再具有覆盖标记;实际分析也变成没有覆盖限定的索引查找。这保持了过滤、排序和索引不变,只改变所需返回列,因而能够明确观察投影变化的影响。实际分析命令会真正执行查询,对真实的大查询使用它时,应把这次诊断本身的执行代价算进去。
最后删除本次实验自己创建的模式:
DROP DATABASE xbcatm_index_lab;
这个小型热缓存实验的时间不能当成吞吐量或延迟基准。调度、缓存和测量本身的开销,可能超过两次二十行查询之间的差别。这里验证的是访问形态与结果内容,而不是承诺物理输入输出减少某个固定比例,更不能据此保证生产请求的高分位延迟必然下降一半。
有意识地承担覆盖成本
给二级索引追加字段会占用存储与缓冲池容量,并在相关索引值变化时增加维护工作。较长的主键也会扩大二级索引条目。应覆盖重要而窄的查询,不要默认把大载荷复制到每个索引里。明确列出返回字段,也有助于后续应用变更在审阅时暴露新的数据访问要求,避免一个查询投影变化悄悄改变成本。
保留新增索引之前,应比较代表性的计划、检查行数、前台延迟以及写入代价。使用真实选择率和并发活动进行验证;安静夹具并没有测量可见性检查需要聚簇访问的频率,也没有测量副本维护这些索引的成本。索引应当通过它实际服务的工作负载证明价值,而不是仅因为一次解释输出里出现了期望的标签。
来源
- MySQL 8.4 聚簇索引与二级索引,二级记录携带主键列的机制,已核验 2026-09-12。
- MySQL 8.4 EXPLAIN 输出,索引使用与覆盖计划标记,已核验 2026-09-12。
- MySQL 8.4 InnoDB 多版本机制,二级索引可见性检查需要聚簇访问的条件,已核验 2026-09-12。