qinghuan
← 回到知识探索

后端现场 / 动手实验

联合索引为什么总在讲“最左边”?

把两列按先后顺序排列,亲眼看见哪些条件能圈出连续的一段。

展开本文目录
  1. 第二列只在第一列相同时继续排序
  2. 打印它们的位置,而不是背口诀
  3. 少了第一列,不等于索引永远没用
  4. 为实际查询挑顺序
  5. 记住这三点
  6. 别把例子用过头
  7. 自己试一小步
  8. 查原始资料
  9. 找一份资料继续练
开始前,会这些就够了
  • 知道表的行和列
  • 看得懂简单 WHERE 条件
遇到不熟悉的词,先看这 1 个解释
索引
数据库为查找数据维护的辅助结构,让某些查询可以缩小要检查的范围,而不必总从头逐行寻找;新增、修改和删除数据时,相关索引也需要维护。
打开完整术语小抄 ↗

假设订单按“店铺编号、下单时间”建立联合索引。查某家店今天的订单很顺手,只查今天所有店铺的订单却未必同样省事。明明查询都用到了索引中的列,差别在哪里?先暂时放下树的层数,看看这些值是怎样排在一起的。

第二列只在第一列相同时继续排序

可以先把索引键想成排好序的二元组:先比较店铺编号,店铺相同再比较时间。这样某家店的记录聚在一段,这家店某个时间范围又是其中的一小段。但“所有店铺中时间为二”的记录,会散在每家店自己的那一段里。

这只是理解键顺序的模型。真实 MySQL InnoDB 的索引还有页、记录和主键信息,字符串比较也受排序规则影响。这里使用整数,让你只观察列的先后关系;不借这个小列表声称磁盘读取次数或实际查询速度。

打印它们的位置,而不是背口诀

程序把四条记录按“店铺、时间、订单号”排序,再找出店铺等于一与时间等于一的记录下标。前一种条件定位到连续位置,后一种条件跨了两个店铺。这解释了为什么固定前面的列,往往更容易把后面的条件变成一段紧凑查找。

SQL 中 AND 条件的书写顺序不是索引列顺序。WHERE time = 2 AND shop = 1,不会仅因为把时间写在前面就失去按 shop、time 查找的能力。数据库看的是条件和索引结构,不能把“最左”理解为 WHERE 语句中最靠左的文字。

小实验 · python
rows = [(2, 1, 201), (1, 2, 102), (2, 2, 202), (1, 1, 101)]
ordered = sorted(rows)
print(ordered)
print("shop=1", [i for i, row in enumerate(ordered) if row[0] == 1])
print("time=1", [i for i, row in enumerate(ordered) if row[1] == 1])

怎样运行:Python 3.10+;保存为 prefix_demo.py,运行 python prefix_demo.py。

运行后对照结果
[(1, 1, 101), (1, 2, 102), (2, 1, 201), (2, 2, 202)]
shop=1 [0, 1]
time=1 [0, 2]

少了第一列,不等于索引永远没用

最左前缀解释的是常见的定位方式,不是所有执行计划的禁令。MySQL 8.4 在满足要求时可以使用 Skip Scan:枚举前面不同的值,再分别查后面的范围。也可能扫描整个索引,或因为索引已经包含所需列而避免再访问完整行。它们和一次直接定位不是同一种成本。

同样,“遇到范围之后的列完全无用”也太绝对。某个条件可能不能继续缩紧主要查找区间,却仍能用于过滤,或作为覆盖查询所需的数据。判断时要分别问:少读了哪些索引记录,少读了哪些完整行,还有没有额外排序。

为实际查询挑顺序

如果主要查询都是“某店铺最近二十单”,shop、time 的顺序值得考虑;如果主要查“全站最近二十单”,需求已经变了。不能只说某列区分度高,就要求它在所有联合索引里排第一,还要看等值、范围、排序与返回列怎样配合。

用固定数据库版本和接近真实分布的练习数据比较 EXPLAIN,再验证返回结果和实际耗时。小表选择全表扫描可能很合理,强行用索引不等于更快。还应记录各店铺的数据量是否悬殊,以及查询一次返回多少行;同一个条件命中十行和命中九成记录,优化器面对的选择并不一样。这里学到的顺序直觉用于提出候选方案,最后仍要让执行计划和测量给出证据。

记住这三点

  • 联合索引先按第一列排列,再在相同值里比较后续列。
  • WHERE 中条件的文字顺序不决定索引列顺序。
  • 索引定位、扫描、过滤、覆盖和排序是不同作用。

别把例子用过头

  • Python 排序不是 B+ 树实现,不能据此推断真实 I/O 次数。
  • MySQL 8.4 的 Skip Scan 有适用条件,不能保证缺前缀时一定出现。
  • 本篇讨论联合索引的列前缀,不是字符串前缀索引的截取长度。

自己试一小步

把列表改为先按时间再按店铺排序,哪一种查询的位置会变得连续?

  1. 每条记录仍按“店铺、时间、订单号”保存,只改变排序时先比较哪一列。查询条件使用的列位置保持不变。
  2. 增加一个同时限制店铺和时间的查询。
  3. 给两个不同查询场景分别提出候选索引,并说明你要查看什么证据。

做到这些,就算完成

  • 没有把条件书写顺序当作索引顺序。
  • 清楚区分“可以使用”与“优化器实际选择”。
  • 没有用四行玩具数据宣称真实性能提升。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。