ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL联合索引最左前缀匹配原理与SQL优化实战

MySQL联合索引最左前缀匹配原理与SQL优化实战 有些 SQL 慢得像老牛拉破车明明索引就在那儿执行计划却不买账。最常碰到的坑之一就是联合索引的左匹配规则——也叫最左前缀匹配。我第一次在 MySQL 的 EXPLAIN 里看到 key 为 NULL 时第一反应是索引没建对后来反复核对字段和建表语句才发现真正的问题是查询条件里没有从联合索引的最左列开始。左匹配规则说的是在联合索引里要利用索引定位数据查询条件中的列必须从索引定义的最左边一列开始连续向后匹配一旦最左列缺失或者中间断了一列后面的列即使出现在 WHERE 里也发挥不了索引查找作用。这篇内容不打算只给定义我会从联合索引的底层存储结构讲起把左匹配规则为什么会存在、哪些 SQL 会失效、如何通过 EXPLAIN 验证、以及设计联合索引时怎么避开这些坑一次说清楚。文章里的案例都来自我实际遇到的慢查询问题适合正在被索引优化困扰的同学也适合想系统理解联合索引原理的开发者。1. 左匹配规则到底是什么1.1 从一个翻车现场说起先看一个特别经典的场景。假设我们有一张订单表建表语句如下CREATE TABLE order_info ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, goods_id INT NOT NULL, pay_amount DECIMAL(10,2), create_time DATETIME, KEY idx_user_goods_pay (user_id, goods_id, pay_amount) ) ENGINEInnoDB;这张表上建了一个联合索引idx_user_goods_pay(user_id, goods_id, pay_amount)。然后我写了一条查询SELECT * FROM order_info WHERE goods_id 1001 AND pay_amount 50;看起来字段都是索引里的列但用 EXPLAIN 一看type是ALLkey是NULLMySQL 选择了全表扫描。为什么因为索引里的三列是有顺序的第一列是user_id。查询条件里没有user_id只写了goods_id和pay_amount而联合索引的数据组织方式决定了 MySQL 无法在没有最左列的情况下快速定位到具体的goods_id区间。这就好比一本电话簿按“姓氏名字”排序你让我直接按名字找某人我只能一页页翻。1.2 说人话版定义左匹配规则正式一点叫“最左前缀匹配规则”。假设有一个联合索引定义为(a, b, c, ...)那么查询要完整利用这个索引WHERE 里的等值或范围条件必须从a开始并按照a → b → c → ...的顺序连续出现。最理想的形态是条件里有a条件里有ab条件里有abc依次类推如果只出现b c索引基本用不上如果出现a c那么只有a能用来做索引查找c只能等数据回表后再过滤。用拉链来类比可能更好理解索引列是一根拉链的链牙必须从最下面那颗链牙开始一颗颗咬合。你跳过第一颗直接拉第二颗、第三颗拉链是合不上的。数据库里的 B 树索引也类似它必须先按第一列确定大致位置再往后进一步缩小范围。1.3 和单列索引的区别单列索引没有顺序问题。只要 WHERE 条件包含这个列并且没有对列做函数处理或隐式类型转换就有机会使用索引。但联合索引不是“多个单列索引的简单拼接”。MySQL 不会因为你在(user_id, goods_id)上建了联合索引就自动支持单独按goods_id查询。很多新人会想“反正 goods_id 也在联合索引里单查 goods_id 应该也能走索引吧”这是最容易踩的误区。如果业务里高频需要单独按goods_id查正确的做法是额外再建一个(goods_id)单列索引或者调整联合索引列顺序。联合索引本身解决的是特定查询前缀链路而不是所有列的所有组合。2. 为什么会有这个规则B树的结构决定了你2.1 联合索引的物理存储结构InnoDB 的索引底层是 B 树。对于联合索引它在叶子节点上并不是把每一列单独存一套数据而是把多列组合成一个“复合键值”按照所有索引列的顺序统一排序。排序规则是先按第一列排第一列相同再按第二列排第二列还相同再按第三列排。例如(a, b)索引数据大致是这样排列的第一组排序值第二组排序值(1, ada)(1, bob)(1, bob)(1, bob)(2, amy)(2, bob)(2, bob)(2, bob)(3, amy)(3, bob)可以明显看到a列是全局有序的b列只有在a值相同的分片内才是有序的。这决定了如果你只按b查B 树的二分检索根本无从下手因为全局没有一套以b为序的排列结构。2.2 为什么缺少最左列就无法二分查找B 树在查找过程中会把查询条件逐步代入内部节点的键值进行比较。比较复合键时数据库会先比较第一列如果第一列的值已经能够决定大小关系就不会再去比较第二列、第三列。打个比方你要在“省市区”三级联动里选择一个区但系统定位依赖的是省、市、区逐级缩小范围。你直接给区名系统无法知道你属于哪个省、哪个市只能把所有区都遍历一遍再判断。索引查找也是这个逻辑第一列是查找的“锚点”没有锚点后面的列再精确也白搭。2.3 查询条件顺序不影响优化器会重排这里要说一个很多文章容易误导的点左匹配规则并不是指 SQL 里写的条件顺序必须和索引列顺序一字不差。MySQL 优化器在生成执行计划之前会分析 WHERE 里的等值条件并对它们进行等价重排。例如SELECT * FROM order_info WHERE goods_id 1001 AND user_id 200;尽管goods_id写在前面但优化器会把user_id 200当成第一个定位条件依然可以走idx_user_goods_pay。真正决定能否用索引的不是书写顺序而是“条件集合里到底包不包含最左列以及中间是否断列”。比如WHERE user_id 200 AND goods_id 1001能走WHERE goods_id 1001 AND user_id 200同样能走。但如果WHERE goods_id 1001 AND pay_amount 50因为缺少user_id再怎么交换也没用。3. 左匹配规则的实际应用哪些查询能用、哪些不能用3.1 索引能生效的几种查询形态为了说明白下面统一假设索引为(a, b, c)。查询条件能否走索引实际使用到索引的列WHERE a 100能aWHERE a 100 AND b 200能a, bWHERE a 100 AND b 200 AND c 300能a, b, cWHERE a 100 AND c 300部分ac过滤无法走索引WHERE b 200 AND c 300不能无WHERE a 100 AND b 200部分ab无法用于索引定位WHERE a LIKE abc% AND b 200部分a前缀范围b可在索引条件下推阶段过滤所以“能用”和“全部用上”不是一回事。实际开发里我们经常用key_len来判断到底用到了联合索引里的哪几列这个技巧后面会单独讲。3.2 会导致失效的典型场景以下几类情况是我在优化慢查询时最常见的失效原因最左列缺失索引是(user_id, goods_id, pay_amount)查询只写goods_id。中间断列索引是(a, b, c)查询写了a和c缺少bc的索引定位能力被浪费。对索引列使用函数例如WHERE DATE(create_time) 2024-01-01只要对列做了函数运算索引列的值在比较前就发生了变化B 树里的顺序帮不上忙。隐式类型转换如果字段是varchar查询里却传了数字MySQL 会先对字段做类型转换再比较同样会导致索引失效。范围条件后面的列WHERE a 100 AND b 200a使用了范围那么后面的b就无法再做精确定位最多被“索引条件下推”当成过滤条件使用。3.3 用 EXPLAIN 验证左匹配光说不练没有意义。下面用回order_info表的实际查询演示。先看一个完全失效的场景EXPLAIN SELECT * FROM order_info WHERE goods_id 1001;执行计划里大概率会出现table: order_info type: ALL key: NULL key_len: NULL rows: 很多行 Extra: Using where再看一个正确使用索引的场景EXPLAIN SELECT * FROM order_info WHERE user_id 200 AND goods_id 1001;执行计划里会出现table: order_info type: ref key: idx_user_goods_pay key_len: 8 rows: 1key_len 8很重要。user_id和goods_id都是INT NOT NULL每个占 4 字节。如果只用了user_idkey_len是 4如果key_len是 8说明goods_id也被用于索引查找了。这就是判断左匹配到底匹配到哪一列的常用方法。4. 左匹配规则的延展与常见误区4.1 范围条件后面的列为什么“断掉”很多同学不理解为什么WHERE a 100 AND b 200不能用b做索引定位。核心原因在于a 100打开的是一个区间B 树能根据这个区间定位到多个叶子节点。但在这些节点里b并没有全局排序只是在每个a值下局部有序。你在一个无序集合里做等值查找只能靠遍历判断而不是二分定位。所以设计联合索引的时候一定要把经常做等值查询的列放在前面把范围查询的列放在后面。比如(user_id, create_time)和(create_time, user_id)前者更适合WHERE user_id ? AND create_time ?后者更适合WHERE create_time ?两者面对的最优查询完全不同。4.2 LIKE 模糊查询也受左匹配影响LIKE能不能走索引和左匹配规则是同一个道理。因为 B 树按字符串从左到右比较的所以WHERE name LIKE 张%可以走索引相当于一个张到张\uffff的范围区间。WHERE name LIKE %张不能走索引因为字符串排序时后面内容是跳着变化的无法从“结尾包含”定位。WHERE name LIKE %张%同样不能通过 B 树直接定位。如果索引是(name, city)查询是WHERE name LIKE 张% AND city 北京这时候name使用了一个范围前缀city不能作为精确查找键使用但 InnoDB 的索引条件下推机制可以在回表前先用city过滤索引项从而减少回表次数。这一点优化也能带来性能收益只是它不属于“查找阶段”的索引能力。4.3 排序与分组同样适用左匹配规则不只影响 WHERE也会影响ORDER BY和GROUP BY。联合索引里的叶子节点已经按多列排好序如果查询条件恰好让索引的最左前缀保持恒定那么后面的排序条件就能直接利用索引顺序避免filesort。例如EXPLAIN SELECT * FROM order_info WHERE user_id 200 ORDER BY goods_id, pay_amount;由于user_id已经等值确定索引内部的goods_id, pay_amount顺序刚好符合ORDER BY所以不需要额外的文件排序。反之如果写成ORDER BY pay_amount, goods_id;或者中间缺少goods_id那 MySQL 就没办法利用这个联合索引的有序性只能先查出来再排序。4.4 救急办法不改变规则有时候我们确实需要一个“歪门邪道”来救急但要清楚这些办法本质上没有改变左匹配规则只是在配合它。覆盖索引索引里包含了查询需要的所有列那么即使部分条件只能消耗到索引前缀后续过滤也可以通过索引数据完成不回表。这会让查询看起来“没全扫”但仍受左匹配限制。调整索引列顺序把业务里最常用的等值条件挪到最左让更多查询能利用同一个索引。补条件如果业务允许在查询里补上缺失的最左列等值条件。比如二级筛选场景中用户选中了某个分类条件自然会加上分类 ID这样索引就能完整使用。加单独索引针对另一个查询前缀再建一个专项索引。索引不是越多越好但这也是最直接的办法。5. 常见问题排查与避坑清单5.1 快速判断索引是否被用到排查慢查询时我习惯按下面三步快速确认左匹配是否生效。第一步看EXPLAIN里的type。从const到eq_ref、ref、range一般说明索引定位参与了如果出现index说明扫描了整个索引树不是按查找条件定位如果是ALL就是全表扫描。第二步看key和key_len。key会告诉你最终命中的索引名key_len需要结合表结构字段长度推算用到了第几列这是最有效的左匹配判断方法。第三步看Extra。Using index表示覆盖索引扫描Using index condition表示启用了 ICP说明部分索引列在引擎层被提前过滤Using where往往代表有些条件是在回表之后才过滤的这些条件很可能就没有用到索引定位能力。5.2 我在一线摸爬滚打遇到的坑这些年排查过不少线上慢 SQL左匹配规则本身不难但和真实业务场景混在一起就容易翻车。第一次踩坑是给一个用户维度的订单表建了(user_id, create_time, pay_amount)索引。运营后台有个报表查询要按create_time统计支付金额结果索引完全用不上每天定时任务跑半小时。后来我单独给create_time建了一个索引才把查询时间降到秒级。第二次踩坑是条件里把范围列放到了联合索引第一位。比如索引设计成(pay_amount, user_id)查询是WHERE pay_amount 100 AND user_id 200结果只有pay_amount参与了索引定位user_id起不到索引查找作用。后来把等值条件user_id放到最左列才让两个条件的索引价值都发挥出来。第三次踩坑是日期条件处理。早期喜欢写WHERE DATE(create_time) 2024-01-01看着很直观但索引失效。改成WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00之后才能正常走范围索引。5.3 联合索引列顺序设计参考设计联合索引顺序时我一般按下面的优先级来考虑等值条件优先查询里出现频率最高的等值列放在最左边。范围条件其次需要做范围比较的列放在等值列的后面。区分度高的列适当靠前如果两列都是等值条件把基数大、区分度高的列放在前面能更快缩小定位范围。考虑排序需求如果查询里有ORDER BY尽量让索引顺序兼容排序字段。按查询集合并集设计索引不要为了几条低频 SQL 建大量联合索引优先让一个索引覆盖多个高频查询。举个例子如果业务上高频查询是WHERE user_id ? AND status ? ORDER BY create_time索引顺序可以优先设计成(user_id, status, create_time)。这样 where 和 order by 都能享受到左匹配的便利。5.4 左匹配速查表下面是针对索引(user_id, goods_id, pay_amount)的速查表方便以后排查时直接对照。查询条件能否走索引分析WHERE user_id 100能只使用第一列WHERE user_id 100 AND goods_id 200能使用前两列WHERE user_id 100 AND goods_id 200 AND pay_amount 50能使用前三列WHERE user_id 100 AND pay_amount 50部分中间缺goods_id只使用user_idWHERE goods_id 200不能缺少最左列user_idWHERE goods_id 200 AND pay_amount 50不能缺少最左列user_idWHERE user_id 100 AND goods_id 200部分user_id是范围goods_id无法用于索引定位最后分享一个我常用的优化思路不管是排查慢查询还是新建索引先把业务里几个高频 SQL 拉出来逐个在纸上标出它们条件里的列再看这些列能不能从左到右连成一条连续的前缀链。能连上就尽量让索引覆盖这些查询连不上就要考虑调整索引列顺序或者补充一个新的索引。左匹配规则看起来很死板真正想通之后它会成为你理解联合索引、设计索引顺序时最直观的判断方式。
返回列表