从一次慢查询说起:读懂最左前缀、覆盖索引与回表

举报
XIANG.FAN 发表于 2026/11/05 10:57:42 2026/11/05
【摘要】 很多慢查询并不是因为数据量突然变大,而是因为查询没有走上预期的索引,或者走上了索引却又做了大量额外工作。本文用一张订单表作为例子,讲清楚排查时最常遇到的三个概念:最左前缀、覆盖索引与回表,并给出可以直接照着做的检查步骤。文中讨论的是 InnoDB 存储引擎下常见的行为,不同版本与配置可能存在细节差异,结论以实际执行计划为准。 一、先从执行计划看,而不是先加索引遇到慢查询,第一步通常不是马上加...

很多慢查询并不是因为数据量突然变大,而是因为查询没有走上预期的索引,或者走上了索引却又做了大量额外工作。本文用一张订单表作为例子,讲清楚排查时最常遇到的三个概念:最左前缀、覆盖索引与回表,并给出可以直接照着做的检查步骤。文中讨论的是 InnoDB 存储引擎下常见的行为,不同版本与配置可能存在细节差异,结论以实际执行计划为准。

一、先从执行计划看,而不是先加索引

遇到慢查询,第一步通常不是马上加索引,而是先看执行计划。执行计划会告诉我们优化器打算用哪个索引、大约要扫描多少行,以及有没有额外的排序或临时表。

EXPLAIN SELECT id, status, created_at
FROM orders
WHERE user_id = 1001 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

重点看几个字段。type 描述访问方式,从好到差大致是 const、ref、range,再往后是 index 和 ALL,其中 ALL 意味着全表扫描,需要重点关注。key 是实际使用的索引,rows 是优化器的估算行数,它只是估算,不是精确值,但在对比改动前后时很有参考意义。Extra 里如果出现 Using filesort,说明需要额外排序;出现 Using temporary,说明需要临时表;出现 Using index,则说明查询可以只用索引完成,不需要回表。

假设订单表是下面这样的结构,并建了一个联合索引:

CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  status VARCHAR(20) NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  created_at DATETIME NOT NULL,
  INDEX idx_user_status_time (user_id, status, created_at)
);

有了这张表,就可以具体讨论索引列的顺序为什么重要。

二、最左前缀:索引列的顺序决定了能用上几列

联合索引可以理解为先按第一列排序,在第一列相同的情况下再按第二列排序,依此类推。最左前缀原则的意思是,查询条件要从索引的最左列开始连续使用,中间不能跳过某一列,否则跳过之后的列就很难再用于索引定位。

上面的索引是 user_id、status、created_at。查询同时给出 user_id 和 status 的等值条件时,两列都可以用于索引定位;再加上按 created_at 排序时,排序也可能直接利用索引顺序,避免额外排序。

如果查询只给出 status,不给 user_id,优化器就无法从索引的最左列开始定位,通常只能全表扫描,或者在其他条件允许时选择别的索引。只给出 user_id 和 created_at,跳过 status 时,user_id 仍然可以用于定位,但 created_at 一般只能用于过滤,而不能像连续使用时那样用于精确定位。这并不意味着 created_at 的条件完全没用,而是它的作用从定位退化为过滤,扫描范围会变大。

范围条件也有类似的截断作用。当某一列使用了范围条件,例如 created_at 在某个时间段内,那么排在它后面的索引列通常就不能再用于索引定位,只能在取到的数据里继续过滤。因此,把经常用于等值查询的列放在前面,把范围查询的列放在后面,往往是更稳妥的顺序。还有一种常见情况是对索引列使用函数,例如对 created_at 做日期截取后再比较,这会让优化器难以直接使用索引。同样,查询值与列类型不一致时发生的隐式类型转换,也可能让索引失效。遇到这类问题,先检查条件是否直接作用在列本身,而不是作用在表达式的结果上。

三、回表与覆盖索引:少取一列,有时差别很大

在 InnoDB 中,二级索引的叶子节点保存的是索引列的值和主键值,而不是整行数据。如果查询需要的列都在索引里,数据库可以直接从索引中得到结果,这就是覆盖索引,执行计划的 Extra 会显示 Using index。如果查询还需要索引之外的列,就要拿主键再回到主键索引里找整行,这个过程就是回表。

回表本身不是错误,点查少量数据时回表的代价很小。问题出现在需要回表的行数很多时:每一次回表都可能是一次随机读,行数一多,总体代价就可能超过直接扫描。优化器在这种情况下有时会干脆选择全表扫描,这也是为什么同样一条查询在数据量小时走索引、数据量大时却不走索引的原因之一。

一个直观的对比是:只查索引里已有的列时,查询可能完全不需要回表;而改成 SELECT 星号,把表中所有列都取出来时,就几乎一定需要回表。实际开发中,列表接口常常只需要少数几个字段,却习惯性地取出全部字段,这就把一次本可以覆盖索引的查询变成了大量回表。把查询改成只取需要的列,既减少了回表,也减少了网络传输的数据量。

覆盖索引也不是越多越好。把很多列都塞进索引,会让索引变宽、写入变慢、占用更多空间,维护成本也会上升。合理的做法是:为高频查询里真正需要的列建覆盖索引,而不是为了覆盖所有查询去建一个无所不包的索引。

四、排序与分页:容易被忽略的第二类代价

当 ORDER BY 的列与索引顺序一致,并且前面的等值条件已经把范围限定住时,排序可以直接利用索引的有序性,不需要额外排序。如果索引顺序与排序要求不一致,数据库就需要把结果取出来再排序,执行计划里会出现 Using filesort。结果集很小时,额外排序的代价并不明显;结果集很大时,排序就可能成为主要开销。

深分页也有类似的问题。LIMIT 加上很大的偏移量时,数据库仍然要先扫描并丢弃前面的行,只返回最后几行。偏移量越大,浪费的工作越多。一种更常用的替代做法是键集分页,也就是记住上一页最后一条记录的排序键,下一页用大于该键的条件继续取数。这种做法要求排序键稳定且有合适的索引支持,不能生搬硬套到所有分页场景,但在按时间或自增主键顺序翻页的列表里效果通常很好。

五、可以照着做的排查清单

第一步,用执行计划确认 type、key、rows 和 Extra,先定位问题是全表扫描、回表太多,还是额外排序。第二步,对照最左前缀检查条件:是否从最左列开始连续使用,有没有跳列,有没有范围条件截断后面的列,有没有在索引列上使用函数或发生隐式转换。第三步,检查 SELECT 列表:只取需要的列,看看能否变成覆盖索引;如果必须回表,评估回表的行数是否在可接受范围。第四步,检查排序与分页:排序是否能利用索引顺序,深分页是否可以改成键集分页。

如果需要调整索引,先想清楚查询模式再动手。把区分度高、经常用于等值条件的列放在前面,把范围列放在后面;一次只改一个因素,改完重新看执行计划,对比 rows 与 Extra 的变化。验证时尽量使用与生产接近的数据量和数据分布,在只有几行数据的空表上,执行计划的结论往往没有参考价值。索引调整涉及写入与存储成本,上线前也需要在测试环境确认写入性能与已有查询没有明显退化。

六、小结

最左前缀回答的是索引能用上几列,覆盖索引回答的是能不能不回表,排序与分页回答的是拿到结果之后还要做多少额外工作。排查慢查询时,先看执行计划,再按这个顺序逐一核对,往往比凭感觉加索引更有效。把查询写清楚、只取需要的列、让条件与索引顺序保持一致,是大多数场景下投入最小、收益最稳定的做法。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。