多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MYSQL索引使用原则(完结)

MYSQL索引使用原则(完结) 最左前缀法则第一步创建实验表我们创建一张order_detail订单明细表并建立一个联合索引(user_id, order_date, product_id)。sql-- 1. 创建表 CREATE TABLE order_detail ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, order_date date NOT NULL COMMENT 下单日期, product_id int(11) NOT NULL COMMENT 商品ID, price decimal(10,2) DEFAULT NULL COMMENT 价格, PRIMARY KEY (id), -- 核心建立一个联合索引顺序为 (user_id, order_date, product_id) KEY idx_user_date_product (user_id, order_date, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 插入几条测试数据 INSERT INTO order_detail (user_id, order_date, product_id, price) VALUES (1001, 2026-08-01, 501, 99.00), (1001, 2026-08-02, 502, 120.00), (1002, 2026-08-01, 503, 45.00);第二步最左前缀法则的核心原理B树排序在联合索引(user_id, order_date, product_id)中数据的排序规则是首先按照user_id排序如果user_id相同则按照order_date排序如果order_date也相同则按照product_id排序。法则口诀查询条件必须从索引的最左列开始并且不能跳过中间的列。一旦跳过某一列该列右侧的列将无法使用索引进行查找但可能会使用索引覆盖扫描这个我们后面讲。第三步实战对比用 EXPLAIN 验证我们通过 5 个常见的 SQL 场景直观看懂“走索引”与“不走索引”的区别。✅ 场景 1完全匹配最左三列完美命中sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01 AND product_id 501;结果key_len较长typeref。结论完全走索引。三个字段都用于缩小范围。✅ 场景 2匹配最左边两列命中前两列sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01;结果走索引idx_user_date_product。结论完全走索引。虽然没查product_id但user_id和order_date依然有序没问题。✅ 场景 3只匹配最左边一列命中第一列sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001;结果走索引。结论走索引。只要包含最左列user_id就会走索引只是效率比场景2低一点。❌ 场景 4跳过中间列最左前缀失效——重点sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND product_id 501;分析条件中有user_id第一列和product_id第三列跳过了第二列order_date。结果key_len只显示用到了user_id的长度比如 4 字节product_id未参与索引下推。结论部分走索引。只有user_id走了索引缩小范围product_id是在回表后或索引扫描时被过滤掉的无法利用索引的有序性。这会导致 Using Index ConditionICP虽然比全表扫描好但无法达到最精准的定位。❌ 场景 5不包含最左列彻底失效sqlEXPLAIN SELECT * FROM order_detail WHERE order_date 2026-08-01 AND product_id 501;结果typeALL全表扫描keyNULL。结论索引完全失效。因为 B 树无法跳过user_id直接去查找order_date因为所有order_date是分散在各个user_id底下的。第四步补充一个极易踩坑的“范围查询”陷阱规则如果最左列中的某一列使用了范围查询,,between,like则该列右侧的列也会停止走索引。sql-- 查询 user_id1001且日期大于 8月1日 的商品 EXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01 AND product_id 501;分析user_id和order_date都走索引但由于order_date是范围查询导致它右边的product_id无法再参与索引查找。结论product_id 501只能作为过滤条件回表后过滤而不是索引定位条件。第五步给你的实战避坑指南总结你的 SQL 中 WHERE 条件顺序索引列顺序为 A,B,C索引利用情况A1 and B2 and C3✅ 全部命中A1 and B2✅ 命中 A 和 BA1✅ 命中 AA1 and C3⚠️仅命中 AB 断了C 无效B2 and C3❌全表扫描缺少最左列 AA1 and B2 and C3⚠️命中 A 和 BB 是范围C 无效特别提醒MySQL 优化器会自动重排如果你写WHERE B2 AND A1优化器会把它变成A1 AND B2所以不需要担心书写顺序关键看字段是否存在。如何救回跳过的列如果你必须查A1 and C3建议把索引改为(A, C)或者单独给C建一个索引。范围查询1. 核心结论务必死磕这句在联合索引中一旦某一列使用了范围查询、、、、BETWEEN、LIKE abc%该列右侧的所有索引列将停止参与“查找Ref”只能退化为“过滤Filter”。2. 为什么范围查询会让右侧列失效B树排序原理还是我们的索引(user_id, order_date, product_id)。想象索引在 B 树叶子节点上的物理排序规则第一优先级user_id升序1, 1, 1, 2, 2...第二优先级当user_id相等时order_date升序8-01, 8-02, 8-03...第三优先级当user_id和order_date都相等时product_id升序501, 502...关键逻辑来了假设你要查user_id 1且order_date 2026-08-01且product_id 502。MySQL 通过user_id1和order_date 2026-08-01在 B 树中定位到了第一个满足条件的起点即 2026-08-02 的第一条数据。接下来MySQL 会沿着链表向后扫描扫描所有user_id1且日期大于 8月1日的记录8-02, 8-03, 8-04...。致命点在这个扫描范围内product_id是完全无序的因为在同一天比如 8-02内product_id是有序的但跨天8-02 和 8-03时product_id的大小关系是乱的。所以MySQL 根本无法利用product_id来做二分查找只能把扫描到的每一行数据拿出来回表后判断product_id是不是 502。3. 用我们那张表做“实验对比”继续使用索引idx_user_date_product (user_id, order_date, product_id)。 场景 A等值查询完美命中三列sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01 AND product_id 501;结果key_len很长假设 43411字节typeref。解读三个列都精准定位直接命中 B 树的一个叶子节点。这是最高效的。 场景 B中间列是范围右侧列失效——重点sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01 -- 范围查询在这里 AND product_id 501; -- 右边的列悲剧了结果key_len只会显示user_idorder_date的长度比如 437字节product_id的长度4字节没有计入key_len。解读product_id501并没有参与 B 树的索引下推ICP查找。MySQL 会先找出所有user_id1001且日期大于 8月1日的所有数据可能 1000 条然后把这 1000 条全部回表再挨个过滤出product_id501的行。性能损耗如果这 1000 条数据分布在不同的磁盘页就会产生大量的随机 I/O。4. 一个极易混淆的“特例”和“坑”坑 1BETWEEN一定是范围查询吗对于普通字段BETWEEN 2026-08-01 AND 2026-08-03等价于和属于范围查询右侧列失效。对于主键/唯一约束如果BETWEEN包含的值极少例如主键 id优化器可能把它当成多个等值查询IN但一般不建议依赖这种优化。坑 2IN查询属于范围查询吗答不属于WHERE user_id IN (1001, 1002)在 MySQL 优化器中通常被处理为多个等值查询相当于OR合并它不会阻断右侧列的索引使用。例外如果IN列表里有成千上万个值优化器可能认为全表扫描更快或者退化为范围扫描此时才可能阻断右侧列。坑 3LIKE通配符的位置决定生死WHERE order_date LIKE 2026-08%—— 这是范围查询相当于 2026-08-01 AND 2026-09-01右侧列product_id失效。WHERE order_date LIKE %2026-08%—— 通配符在前面索引彻底失效连order_date都用不上更别提右侧列了。5. 为了绕开“范围阻断”高手怎么调优如果业务必须按照user_idorder_date范围product_id来查询该怎么办方案一改索引顺序把等值条件放左边范围条件放最后。把索引改为(user_id, product_id, order_date)。这样user_id和product_id精准定位order_date只负责范围扫描。右侧没有列了互不干扰方案二覆盖索引不回表如果你只查询索引中包含的字段比如SELECT user_id, order_date, product_id虽然product_id不能用于查找但可以通过覆盖索引Using index直接返回数据避免了回表的随机 I/O性能损耗大幅降低。6. 给你一张极简的“红绿灯”速查表针对联合索引 A, B, CSQL 中 WHERE 的条件索引利用情况执行效率评级A 1 and B 2 and C 3命中 A, B, C⭐⭐⭐⭐⭐精准打击A 1 and B 2 and C 3命中 A, BC 失效⭐⭐⭐扫描范围变大A 1 and B in (2,3) and C 4命中 A, B, CIN 不算范围⭐⭐⭐⭐多个精准命中A 1 and B 2 and C 3仅命中 AB、C 都失效⭐⭐扫描大量数据最后给你一句保命口诀“等值写在最前面范围写在最后面一旦范围出现后右侧列全都不顶用。”覆盖索引1. 什么是覆盖索引一句话定义覆盖索引指SELECT 查询的字段直接全部包含在你建立的索引树中MySQL不需要回表去主键索引聚簇索引里取数据直接从索引树里把数据拿走。口语化比喻没有覆盖索引你去图书馆查书索引卡上写着“书在3楼5号架”你得跑过去把书拿来回表。有覆盖索引索引卡上直接印着这本书的全部内容你连书架都不用去看一眼索引卡就完事了不回表。2. 如何判断是否用了覆盖索引看执行计划在EXPLAIN结果中Extra列如果出现Using index就代表这条查询用了覆盖索引。特别注意Using index和Using index condition索引下推 ICP是两码事。前者是“不回表”后者是“回表前先过滤”性能差了一个数量级。3. 实验对比基于我们的表我们的表字段有id主键、user_id、order_date、product_id、price。我们的索引是idx_user_date_product (user_id, order_date, product_id)。 场景 1没有覆盖索引需要回表sqlEXPLAIN SELECT * FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01;分析索引树里只有user_id、order_date、product_id和主键id。但SELECT *需要取出price字段索引树里没有price。结果Extra显示Using index condition或者Using where。MySQL 必须拿着查出来的主键id回主键索引树里去把price取出来。这是随机 I/O很慢。 场景 2完美覆盖索引不回表 —— 救回“范围查询”的经典用法我们把刚才那条 SQL 的SELECT *改成只查索引中包含的字段sqlEXPLAIN SELECT user_id, order_date, product_id FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01;分析虽然order_date用了范围查询导致product_id不能用于缩小查找范围上节课内容。但是因为SELECT只要user_id、order_date、product_id这三个字段全都长在索引树上。MySQL 直接扫描索引树range类型把扫描到的行直接返回完全不需要回表。结果Extra显示Using index。性能等级从“慢查询”直接拉升到“飞快”。 场景 3覆盖索引 最左前缀缺失依然能救急如果我们非要查order_date和product_id但没带最左列user_id导致索引无法用于查找但查询字段全在索引里sqlEXPLAIN SELECT user_id, order_date, product_id FROM order_detail WHERE order_date 2026-08-01;分析虽然order_date不是最左列正常情况下会全表扫描。但这里 MySQL 觉得索引树比全表聚簇索引小得多直接扫描整个联合索引树type index就能拿到所有数据不用回表。结果Extra显示Using index。虽然它扫描了全索引不是最优但比全表扫描快但依然避免了最严重的回表开销。4. 覆盖索引的“终极大坑”SELECT *刚才的例子告诉我们覆盖索引最大的天敌就是SELECT *。只要你的SELECT里多了一个不在索引里的字段比如priceUsing index就会立刻消失变成回表。尤其是在order_date 这种范围查询下如果回表扫描的行数可能成千上万随机 I/O 会直接把数据库拖垮。5. 实战避坑如何利用“覆盖索引”优化慢查询假设业务需求必须查用户ID、日期范围、商品ID、和价格price。sql-- 这是慢查询原版 SELECT user_id, order_date, product_id, price FROM order_detail WHERE user_id 1001 AND order_date 2026-08-01;优化方案 1偷懒修改索引推荐把索引改成(user_id, order_date, price, product_id)或者(user_id, order_date, product_id, price)。这样price也被塞进了索引树SELECT的所有字段都在索引里立刻触发Using index。优化方案 2拆分成两步迫不得已先走覆盖索引查出主键idSELECT id FROM order_detail WHERE user_id1001 AND order_date ...秒出。再用查出来的少量id去IN查询回表拿price因为这时候id是主键回表是顺序读很快。6. 覆盖索引 VS 索引下推ICP—— 执行计划速判表EXPLAIN 的 Extra 字段中文含义是否回表效率评价Using index覆盖索引❌ 不回表⭐⭐⭐⭐⭐极致快Using index condition索引下推ICP✅ 回表但过滤了部分数据再回⭐⭐⭐中等Using where普通过滤✅ 回表且大量回表⭐很慢7. 给你的终极口诀结合前两节课最左前缀管查找范围查询断右道若想查询飞起来SELECT只把索引要一旦出现Using index回表开销全扔掉。最后送你一个习惯以后写查询尤其是在联合索引下先看一眼SELECT的字段。如果发现多了一个不在索引里的“捣蛋鬼”字段问自己一句我能不能把它也加进索引里或者干脆不查它这一个习惯能帮你避开 90% 的性能陷阱。前缀索引create index idx_XXX on table_name(column(n));选择性select count(distinct substring(phone,1,4)) / count(*) from user_info;
返回列表