上一题下一题

请结合一个实际项目,说明你如何为 MySQL 核心业务表设计联合索引,并通过执行计划验证索引是否生效;请进一步说明最左匹配、回表和索引覆盖对最终方案的影响。

免费版
来源:alibaba初级工程师索引覆盖与回表联合索引设计SQL优化验证执行计划分析
答案解析

以订单查询为例,核心业务表 orders 大概有几千万数据,常见查询是按租户、用户、订单状态和创建时间筛选,并按创建时间倒序分页:

我会先结合线上慢查询和业务访问模式确定索引,而不是把查询条件里的每个字段都简单建成索引。这个场景下可以考虑联合索引:

索引顺序主要考虑几个因素:tenant id 和 user id 区分度较高,而且通常是等值查询;status 也是等值条件,但区分度相对低,所以放在等值条件之后;created at 用于时间范围过滤和排序,放在后面。这样可以先快速定位租户和用户,再过滤状态,并尽可能利用时间字段完成范围扫描和排序。

验证时我会先执行:

重点关注几个字段:

key 是否选择了预期的联合索引; possible keys 是否包含这个索引; key len 是否使用到了联合索引的多个字段; type 是否至少达到 range,如果前面都是等值条件,通常会出现范围扫描; rows 和 filtered 是否合理; Extra 中是否出现 Using filesort 或 Using temporary; EXPLAIN ANALYZE 中实际扫描行数和实际耗时是否符合预期。

如果 key 没有选择这个索引,我会检查统计信息是否过期、字段类型是否一致、是否对索引列进行了函数或隐式类型转换、数据分布是否导致优化器认为全表扫描成本更低,也会确认查询条件是否真的满足索引顺序。必要时可以执行 ANALYZE TABLE orders 更新统计信息,但不会一开始就使用强制索引。

最左匹配原则会直接影响索引设计。对于索引 (tenant id, user id, status, created at),只有从最左列开始连续匹配,索引才能充分发挥作用。例如查询包含 tenant id、user id 和 status 时可以使用;只查询 user id,通常不能有效使用这个索引;如果跳过了 user id,即使查询了 tenant id、status 和 created at,也只能有效利用到 tenant id,后面的字段不一定能继续用于定位。索引字段的书写顺序不要求和 SQL 条件顺序一致,优化器会根据条件匹配索引,但索引本身的列顺序必须符合最左匹配规律。

还要注意范围条件会影响后续列的使用。比如 created at 放在最后比较合适,因为它是范围条件。如果把范围字段放在 status 前面,范围扫描之后的列通常不能继续用于缩小索引扫描范围,可能导致扫描更多数据。对于排序来说,前面的索引列需要有确定的等值条件,才能比较稳定地利用后面的 created at 顺序;如果前面存在范围条件或排序字段与索引顺序不匹配,就可能出现 Using filesort。

回表是指二级索引中只保存索引列和主键,使用联合索引定位到记录后,还要根据主键回到聚簇索引中取出其他列。例如上面的查询需要 total amount,如果它不在联合索引中,MySQL 找到符合条件的主键后,还要回表读取 total amount。当匹配记录很多时,回表次数多,随机 I/O 成本会比较明显。

如果这个查询非常高频,而且返回字段固定,我会评估把 order no、total amount 等必要字段加入索引,形成覆盖索引,例如:

这样查询需要的字段都能从索引中取得,可以减少回表,执行计划中通常会看到 Using index。但覆盖索引不是越宽越好,它会增加索引体积、写入和更新成本,也会影响缓存命中率,所以只针对高频、稳定的查询做评估。另外,id 通常是主键,在二级索引中本身已经存在,不一定需要显式重复添加,具体还要结合 MySQL 版本和索引结构确认。

最终方案我会在接近真实数据量和分布的环境验证,包括有数据和无数据、不同租户规模、不同时间范围、分页深度等情况,并对比加索引前后的实际耗时、扫描行数和执行计划。对于深分页,还会考虑使用基于 created at 和 id 的游标分页,避免 LIMIT 100000, 20 扫描并丢弃大量数据。上线后继续观察慢查询、索引使用情况和写入性能,确认这个索引既解决查询问题,也没有给写入和存储带来不可接受的成本。