☰
索引建了一堆,查询为什么还是慢?——联合索引与最左前缀原则,一次讲透
2026/10/7 1:30:42 网站建设 项目流程

你给订单表建了个联合索引(user_id, create_time, status),信心满满。

结果上线后,一条WHERE create_time > '2026-09-01' AND status = 1的查询,还是把整张表扫了一遍。DBA 瞟了一眼你的 SQL,甩过来四个字:“最左前缀。”

你一脸懵:索引我建了呀,三列都在里面呀,凭什么不走?

问题就出在——你以为"建了联合索引 = 三列都能用",可实际上,联合索引是一根"有顺序的链条",它只对"从最左边开始"的查询有用。

这一篇,把这件事从头到尾掰开。

索引凭什么快?因为它"有序"

先回到最底层的那个直觉(我之前在《数据库索引为什么用 B+ 树》里聊过 B+ 树的结构,这里只讲结论):索引的本质,是一份排好序的"目录"。

一本字典,你能秒查"苹果"这个词,是因为所有词条按拼音排了序。排序,让你能"对半切",每次排除一半,而不是一页一页翻。

单列索引排序很好理解:age列建索引,就是把所有行的age从小到大排好。查age = 18,顺着排好序的链表/树,几下就定位到了。

可联合索引呢?多列怎么"排成一列"?

答案是:按字典序,一列一列地排。

联合索引(a, b, c)的排序规则是:

  1. 先按a排序;
  2. a相同的行,再按b排序;
  3. a、b都相同的行,再按c排序。

说白了,就是把(a, b, c)当成一个"三位的号码",逐位比较。这跟你手机通讯录先按姓氏、再按名字排,是一模一样的道理。

关键来了:第二列只在"第一列相同"时才有序

这是理解最左前缀的唯一钥匙,请盯住这句话:

在联合索引里,b列不是全局有序的,它只在"a相同的那一小段里"才有序。

举个例子,联合索引(a, b)排出来的顺序长这样:

(a, b) (1, 2) (1, 5) (1, 9) ← 在 a=1 这一段里,b 是 2、5、9,有序 (2, 1) (2, 3) (2, 8) ← 在 a=2 这一段里,b 是 1、3、8,有序 (3, 4) (3, 6) ← 在 a=3 这一段里,b 是 4、6,有序

现在你问它:b = 5在哪?

你顺着整个索引扫一遍b列:2, 5, 9, 1, 3, 8, 4, 6——这是乱的!因为每个a值都会让b重新从"它自己的最小值"开始排。

既然b这一列整体是乱的,索引就没法用"有序"这个本事去对半查找b,只能从头扫到尾。这就是"b = 5用不上(a, b)索引"的底层原因。

反过来,a = 2 AND b = 3就能用:先靠a的有序定位到a=2那一段,再靠这一段里b的有序定位到b=3。两步都踩在"有序"上。

于是,最左前缀原则就出来了

把上面的逻辑翻译成人话,就是那条著名的规则:

联合索引(a, b, c),等价于三个"缩小版"索引:(a)、(a, b)、(a, b, c)。但凡是没从a开始的查询,都用不上它。

对照着看,这几条 SQL 的"命中情况"一目了然:

SQL 条件命中索引的列为什么
WHERE a = 1 AND b = 2 AND c = 3a、b、c 全命中从最左开始,逐列有序
WHERE a = 1 AND c = 3只有 a中间跳过了 b,c 在"a=1 且 b 不定"的前提下是乱的
WHERE b = 2一列都没有没从最左列 a 开始
WHERE a > 1 AND b = 2只有 a(范围)a 一旦是范围,b 就"断档"了

最后一行最值得细说,因为它最反直觉。

为什么"范围查询"会让后面的列断档

WHERE a > 1是什么?是"一段区间",不是"一个点"。

定位到a > 1后,命中的是所有a = 2、3、4……的行。这些行里,b的取值,是"在 a=2 里排一遍、再在 a=3 里排一遍、再在 a=4 里排一遍……"——每一段内部有序,段与段之间又是乱的。

所以a > 1 AND b = 2,索引只能帮你锁定"a > 1 那一大段",b = 2这个条件没法利用有序性,只能在那一段里逐行过滤。

这就是为什么老 DBA 会念叨:范围查询要放在联合索引的"最后一列"。如果你经常按(a = 某个值, b 范围)查,就该建(a, b);如果还经常(a, b 范围, c 等值)查,那c注定吃不到索引,得另外想办法。

一句话记住:索引是"字典",你得"从第一页开始查"

你可以把联合索引想象成一本多级目录的电话簿:先按"省"分,再按"市"分,再按"姓名"分。

  • 你说"查江苏省的人"——能用(第一列);
  • 你说"查江苏省南京市的人"——能用(第一、二列);
  • 你说"查所有姓王的人"——用不了!因为姓王的人散落在每个省、每个市里,目录根本没按"姓"整体排过序。

最左前缀,说的就是:你要用这本目录,就必须从"省"这一级开始往下查,不能跳级,也不能从中间某一级开始。

光知道"为什么失效"还不够,再补两刀

第一刀:列上套函数/运算,索引立刻失效。比如WHERE YEAR(create_time) = 2026,索引里存的是原始create_time,你给它套了个YEAR(),排序的"钥匙"就变了,索引不认识,只能全表扫。改成WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01',就又能用上了。同理,WHERE age + 1 = 20要改成WHERE age = 19。

第二刀:隐式类型转换,是隐蔽的索引杀手。phone是字符串列,你写WHERE phone = 13800138000(数字),数据库得把每一行的phone都转成数字再比较,这一转,索引就废了。写WHERE phone = '13800138000'(带引号)才对。

这些"失效"场景,本质和最左前缀是同一件事:索引是一份"按特定规则排好序的目录",一旦你的查询条件破坏了那个排序规则(跳列、套函数、改类型),目录就没法用了。

顺手把"回表"和"覆盖索引"也说清

有了最左前缀的基础,这两个八股概念就顺理成章了。

联合索引(二级索引)的叶子节点里,存的是索引列 + 主键值,不是整行数据。所以:

  • 你查SELECT * FROM t WHERE a = 1,先靠索引找到主键,还得再回主键索引(聚簇索引)查一遍,才能拿到整行——这叫回表,多跑一趟;
  • 你查SELECT a, b FROM t WHERE a = 1,要的列a、b索引里全有,不用回表,直接返回——这叫覆盖索引。

所以面试官问"为什么要避免SELECT *",一半是为了省网络,一半就是为了能走覆盖索引、少回表。这里面的链条,全是 B+ 树结构推出来的,不是死记的八股。

结语:先把"为什么",再背"是什么"

最左前缀原则,背起来就一句话;但真正理解它,靠的是往下多想一层——索引快的本质是"有序",联合索引的"有序"是逐列嵌套的,所以只有从最左开始、逐列对齐的查询,才踩得上有序性。

下次再看到"索引失效",别急着翻八股清单,先问自己一句:这条查询,还能不能用上"有序"?从哪一列开始,有序就断了?想清楚这个,你甚至不用背规则,自己就能推出来。

数据库索引、查找、排序这些,底层都长在"数据结构"这门课上。想把这些串成体系、把 408 的"查找"章节吃透,推荐 B站【408实验室】的《数据结构》系统跟学,配着 CoLearn(yantucs.com)的 AI 答疑和错题集,边学边问,事半功倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询