ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL索引优化实战:从B+树原理到失效场景与EXPLAIN排查

MySQL索引优化实战:从B+树原理到失效场景与EXPLAIN排查 在数据库里折腾久了就会发现一个规律大部分慢查询最后都能绕回到同一个词——MySQL索引。作为开发者你可能已经在CREATE INDEX或者ALTER TABLE里加过索引也可能在面试中被问过“最左前缀”“回表”“索引失效”甚至线上出过事一条更新语句把整张表锁住了或者一条看似简单 select 突然把 CPU 打满。这些问题的根源往往不是 MySQL 本身而是对索引的底层机制理解不透。这篇文章我不打算把索引摘成一本字典而是按实际使用场景来讲先讲索引的存储结构再讲设计索引时怎么选列、怎么排顺序然后重点拆解那些让索引“白建”的经典场景最后用 EXPLAIN 和真实案例把排查链路串起来。文章也会带上索引和锁的关系以及几道高频面试题的速答思路。无论你是写了两年 CRUD 想往深处走的开发还是刚接手一个慢 SQL 成堆的老系统这篇应该都能给你一些能直接落地的东西。1. 索引到底是什么从磁盘 I/O 到 B 树的那些事1.1 没有索引的查询为什么慢得让人抓狂很多人对“慢”没有直观概念觉得无非就是多等几百毫秒。可一旦表里数据上了千万级没有索引的查询就不是几百毫秒的事了很可能是十几秒甚至直接卡死业务。问题出在磁盘 I/O。InnoDB 存储引擎的数据存放单位是页默认一页 16KB每次读写磁盘基本以页为单位。在没有索引的情况下要找到某一行MySQL 只能从第一个数据页开始一页一页读出来再逐行比对过滤条件。哪怕你能通过 WHERE 条件筛掉绝大多数数据引擎也不知道都在哪个页里只能老老实实把整张表扫一遍。把这个场景换成生活里的动作就很好理解了一本几千页的书没有任何目录你要找某个知识点只能从第一页翻到最后一页运气好翻得快运气差就是来回翻。索引相当于给数据加了一套目录让 MySQL 可以“跳过大部分页码直达目标”。当然全表扫描也不是一无是处。当表很小比如只有几百行或者查询条件压根筛不出多少数据时全表扫描反而比走索引更快。因为走索引有额外的“查目录 回表”成本优化器不傻它会根据统计信息估算成本。这也是后文讲“优化器看走眼”时要聊的伏笔。1.2 B 树为什么成了 InnoDB 的默认选择索引结构不是只有一种面试题里经常拿出来对比的有哈希索引、B 树索引、B 树索引、红黑树还有 R 树之类的空间索引。InnoDB 默认选择 B 树背后是有明确理由的。先说哈希索引。哈希对等值查询极其高效一次计算直接定位时间复杂度 O(1)。但它有两个硬伤第一没法做范围查询WHERE age 20这种条件在哈希索引里无从下手第二哈希索引不支持排序ORDER BY也顶不住。研发系统时查询条件往往是“大于等于”“between”“order by”混合的哈希结构应付不了。再说普通 B 树。B 树的每个节点既存索引键也存数据指针导致非叶子节点的扇出一个节点能指向子节点的数量变小。扇出变小意味着同样的数据量树的高度会更高查询时要访问的节点更多磁盘 I/O 次数也随之增加。B 树在 B 树基础上做了关键调整非叶子节点只存索引键和子节点指针不存数据所有数据都集中在叶子节点并且叶子节点之间用双向链表串联。这样做有三个直接好处非叶子节点能存放的键数量大幅增加树的高度更矮。实际场景里三层 B 树就能撑起千万甚至上亿行的数据量。范围查询可以直接沿着叶子节点的链表顺序遍历不用反复回溯父节点效率极高。每次磁盘 I/O 能取回更多“有效索引信息”命中率更高。简单算一下三层 B 树能存多少数据。假设一页 16KB一行数据 1KB那么一个叶子页能存约 16 行。非叶子节点里假设每个键占 8 字节每个子节点指针占 6 字节加起来约 14 字节一页能放约 1170 个键。两层根节点能指向 1170 个叶子页三层就能覆盖 1170 × 1170 × 16约 2100 万行。这个量级对绝大多数业务表都够用了。这也是为什么你很少看到高度超过四层的 B 树——数据量没到那个份上。1.3 聚簇索引与非聚簇索引一次查询背后可能有两次查找InnoDB 里每个表都有一个聚簇索引clustered index它决定了表中数据的物理存储顺序。如果你定义了主键主键就是聚簇索引如果没有主键InnoDB 会选一个非空唯一键来顶替两者都没有它会在内部生成一个隐藏的 row id 作为聚簇索引。聚簇索引的特点是索引键值本身和整行数据放在同一个叶子节点里。所以通过主键查询时一次 B 树定位就能拿到整行记录不需要额外的数据读取步骤。问题出在二级索引secondary index上。你为了加速查询建的普通索引叶子节点里存放的不是整行数据而是“索引列的值 主键值”。这意味着如果查询条件用到了二级索引但你要的字段除了主键之外还有其他列MySQL 就得先从二级索引 B 树里找到主键值再拿着主键值到聚簇索引里做第二次查找才能取回完整数据行。这个行为有一个专门的名字回表table lookup。回表不是每次都会发生。如果你的 SELECT 列表里只有索引列和主键那么二级索引的叶子节点已经包含了全部所需字段MySQL 就不需要回表。这种情形称为覆盖索引covering index执行计划里的 Extra 通常会显示Using index。设计索引时尽量让高频查询“覆盖”住是压查询延迟最有效的手段之一。举个直观例子。-- 假设表结构id 主键有 (user_id, status) 联合索引 -- 这条 SQL 只需要 user_id、status、id联合索引里全都有无需回表 SELECT id, user_id, status FROM orders WHERE user_id 10086; -- 这条 SQL 需要 amountamount 不在索引里必须先找到主键再回表 SELECT id, user_id, status, amount FROM orders WHERE user_id 10086;单独把“回表”拎出来讲是因为很多人加索引只看 WHERE 字段忽略了 SELECT 字段结果优化效果打了折扣还找不到原因。2. 索引设计与创建的最佳实践2.1 哪些列适合建索引哪些列坚决不建索引能加速查询但每次写入INSERT、UPDATE、DELETE时都要维护索引。数据量一大索引建得太多写入性能会肉眼可见地下降。所以建索引之前先想清楚哪些列值得。适合建索引的列通常有这几个特征经常出现在 WHERE 条件里经常作为 JOIN 的关联字段或者经常被ORDER BY、GROUP BY使用。反过来那些频繁更新的列、字段取值很有限的列、长度很长的文本列都要慎重。尤其是区分度。所谓区分度就是某一列不同值的数量占总行数的比例。计算方式很简单SELECT COUNT(DISTINCT column_name) / COUNT(*) FROM table_name;这个比值越接近 1说明这一列的值越五花八门索引筛选能力越强。如果一个列只有“男”“女”两种取值区分度约等于 0.5索引根本帮不上忙——因为筛选一半数据还不如直接全表扫。常见的反例就是在性别、状态这类低基数列上建索引最后走了索引反而更慢。文本列也不是完全不能建索引但最好不要直接对很长的 VARCHAR 列建完整索引。比如一篇文章的标题动辄几百个字符索引也会很大占空间也拖慢写入。这时可以截取前 N 个字符建前缀索引既能在一定程度上过滤数据又不会让索引膨胀得太夸张。ALTER TABLE article ADD INDEX idx_title (title(20));但要注意前缀索引无法用于ORDER BY和覆盖索引扫描只能加速 WHERE 过滤。如果业务强依赖排序老老实实建完整列索引。2.2 复合索引顺序与最左前缀原则复合索引也叫联合索引是面试和实操中的“核心考点”。它指的是用多个列共同组成一个索引比如订单表上建(user_id, status, create_time)。复合索引遵循最左前缀原则查询条件中使用索引列的顺序必须从索引最左边的列开始连续匹配这个索引才可能被完整或部分使用。比如上面的索引能命中这些查询WHERE user_id ?WHERE user_id ? AND status ?WHERE user_id ? AND status ? AND create_time ?但下面的查询就尴尬了WHERE status ?—— 跳过了最左列 user_id无法使用该索引。WHERE user_id ? AND create_time ?—— 只能用到 user_id 这一列create_time 部分会被跳过除非 MySQL 8.0 启用了索引跳跃扫描且条件满足。很多人把最左前缀理解成“WHERE 里写了哪些列索引就能命中哪些列”这是不准确的。准确说法是从索引第一列开始连续命中一旦中间断了后面的列都白搭。设计复合索引时列的顺序通常按两个原则排把区分度高的放前面把查询频率高、且经常作为等值条件的列放前面。具体怎么权衡我一般先看等值条件再看排序字段。一个典型的坑是查询条件里 user_id 是等值create_time 是范围。比方说WHERE user_id ? AND create_time ?。如果索引建成(create_time, user_id)那么 create_time 的范围条件会让后面的 user_id 无法发挥索引过滤作用如果建成(user_id, create_time)那么 user_id 完成精确匹配后create_time 仍可继续走索引范围扫描。这就是为什么“等值列放前面、范围列放后面”是复合索引设计的默认口诀。2.3 覆盖索引、前缀索引与函数索引覆盖索引前面提过它本质上是一种“收益白送”的索引。如果一条 SELECT 需要的字段恰好全部包含在索引里InnoDB 就不需要回表。考虑到回表大概率伴随一次额外的主键 B 树查找覆盖索引在千万级表上的性能优势非常明显。实战里我见过不少查询是SELECT id, title, status FROM article WHERE status ?建了(status)索引但查询要走回表。如果把索引调整为(status, id, title)或(status, title, id)让查询需要的列全在索引叶子节点中执行计划里的 Extra 会显示Using index查询耗时往往能下降一个数量级。当然也不要为了覆盖而无脑把所有 SELECT 列塞进索引毕竟索引体积膨胀也会影响写入和存储。函数索引则要啰嗦两句。MySQL 8.0 之前对索引列使用函数或表达式会导致索引失效这个我在下一章会详细讲。8.0 开始支持函数索引可以这样建ALTER TABLE user ADD INDEX idx_create_date ((DATE(create_time)));建完以后WHERE DATE(create_time) 2025-01-01就能命中这个索引。不过函数索引本质上是把计算结果存到了索引里存储成本和写放大需要你自己权衡。能用普通索引的范围查询解决我建议优先改造 SQL而不是无脑建函数索引。2.4 建索引的方式DDL、在线变更与索引体检建索引的 SQL 本身很简单三句话搞定-- 建表时直接指定索引 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, status TINYINT NOT NULL, create_time DATETIME NOT NULL, INDEX idx_user_status (user_id, status) ) ENGINEInnoDB; -- 追加索引 ALTER TABLE orders ADD INDEX idx_user_status (user_id, status); -- 或者用 CREATE INDEX CREATE INDEX idx_user_status ON orders (user_id, status);真正需要注意的是大表上建索引的时机。例如一张线上表已经有两千万行直接ALTER TABLE ... ADD INDEX会持锁并阻塞写入业务可能中断几十秒甚至几分钟。MySQL 8.0 里某些 DDL 支持ALGORITHMINSTANT和ALGORITHMINPLACE但新增索引依然可能在准备阶段短暂需要元数据锁。稳妥的做法是用在线变更工具比如pt-online-schema-change以触发器方式逐批拷贝数据、控制速度把锁表窗口压到几乎为零。经验不足的时候优先挑业务低峰期操作并把变更拆成小批次别在白天大流量时硬刚。建完索引也不是高枕无忧。建议用SHOW INDEX FROM table_name看看基数Cardinality这个数值是优化器判断索引好坏的参考。如果基数明显小于实际行数或者统计信息过期可以ANALYZE TABLE table_name重新收集统计信息。另外冗余索引要定期清理比如已经有了(user_id, status)再单独建一个(user_id)就是重复的后者完全没必要存在。3. 索引失效的经典场景与底层原因这一章是全文最重要的安慰剂也是在座各位最容易“翻车”的地方。索引明明建了查得还是慢十有八九是走进了某个失效场景。我把高发场景拆开讲每个都配一个能复现的 SQL 和原因分析。3.1 隐式类型转换一个引号引发的全表扫描先看一段 SQLSELECT * FROM user WHERE phone 13900001111;假设 phone 列是 VARCHAR查询条件里写的是数字。MySQL 在比较时会做隐式类型转换把字符类型的 phone 先转成数字再比较。因为索引保存的是字符串值一旦比较前先做了函数转换B 树里已经排好序的字面值就和查询值“对不上号”了索引自然失效回退成全表扫描。这种现象在代码里太容易产生了。后端从请求参数里取出手机号或身份证号格式化时没转字符串直接当数字拼进 SQL或者 Java 里的 Long 参数传给了 VARCHAR 列。排查方法也简单EXPLAIN看执行计划如果key为 NULL 或者 type 是 ALL再看条件两边是不是类型不匹配。对应解法就是让条件值类型和列类型一致写成phone 13900001111索引立刻就能用上。养成一个习惯所有字符串列在拼 SQL 时都带引号不要依赖 MySQL 帮你转。3.2 函数与表达式包裹索引列类似的失效场景还有WHERE DATE(create_time) 2025-01-01。索引是对 create_time 原始值建立的但查询条件是经过 DATE() 函数加工后的值优化器无法绕过函数直接用索引只能对每一行计算函数后过滤。破解办法有三条路优先级我做过排序如果业务对精确时间不敏感把条件改成范围查询create_time 2025-01-01 AND create_time 2025-01-02。这样完全走原始索引性能最好。8.0 环境可以建函数索引直接把DATE(create_time)的计算结果索引化。如果字段本身就是按天拆分的冗余列比如专门存一个 date 列那直接对这个冗余列建索引。另外一个容易被忽略的对索引列做数学运算也不行比如WHERE price * 0.8 100。把表达式挪到等号右边price 100 / 0.8索引就能命中。本质逻辑很简单——谁被函数套住了谁就出局。3.3 模糊匹配、OR、NOT IN 与范围查询的杀伤力模糊匹配里LIKE abc%可以用索引因为可以依托 B 树的排序直接找前缀区间但LIKE %abc或LIKE %abc%没法利用索引的顺序结构优化器只能全表扫。OR 的坑更典型。WHERE name 张三 OR status 1如果 status 没有索引整个查询可能直接放弃 name 索引改走全表扫描。不是说 OR 一定不能用索引而是优化器很难把两个条件的结果做高效的索引Merge尤其在其中一个条件无法走索引的时候索性全部放弃。最稳妥的改写方式是拆成两条 SQL 用 UNION ALL 合并或者保证 OR 两边的条件都有独立索引让优化器有路可选。NOT IN、、!也是类似处境。B 树对于范围“排除”的检索效率并不高优化器评估下来全表扫描可能更划算。如果业务确实需要排除少量数据反转思路用包含条件代替排除条件通常能拿到更优的执行计划。范围查询还有一个很隐蔽的坑体现在复合索引里范围右边的列会失效。比如索引是(user_id, create_time, status)查询WHERE user_id 1 AND create_time 2025-01-01 AND status 1user_id 和 create_time 能走索引但 status 发挥不了作用因为 create_time 的范围条件让 status 无法继续做精确定位。解决办法是把范围列尽量往后放或者把 order by 字段、等值字段设计在范围列之前。3.4 优化器“看走眼”与统计信息失真索引失效不全是 SQL 写法的问题优化器自己也会“判断失误”。InnoDB 统计索引基数Cardinality不是实时精确计算而是基于采样估算。数据大量增删后统计信息可能严重过期优化器误以为走某个索引要扫描很多行于是走了全表扫描。遇到这种情况第一反应不是骂优化器而是先跑一下ANALYZE TABLE orders;这个命令会重新计算索引基数让优化器拿到相对准确的数字。另外还有一种情况小表确实不值得走索引。表只有几千行全表扫描只需要几个 I/O而走索引要先查 B 树再做回表累计成本反而更高。此时 type 为 ALL 不一定是出了问题关键看 rows 估算和实际耗时。还有一类比较棘手的是索引列存在大量 NULL 值。WHERE column IS NULL在部分版本和特定条件下可以借助ref_or_null访问方式走索引但IS NOT NULL通常会失去索引优势。设计表结构时能设置NOT NULL DEFAULT的尽量设别留一堆语义不明的 NULL 在索引列里找麻烦。4. 用 EXPLAIN 和实际案例定位索引问题4.1 EXPLAIN 核心字段阅读type、key、rows、Extra排查索引问题最常用的工具就是EXPLAIN它展示 MySQL 执行一条 SQL 时的计划信息。直接加在查询前执行即可EXPLAIN SELECT user_id, status, create_time FROM orders WHERE user_id 10086;不需要任何图形化工具命令行客户端就能跑。核心字段我列一下字段含义面试/实操中的关注点type访问类型从好到坏大体是 const eq_ref ref range index ALL。看到 ALL 就要警惕全表扫描key实际用到的索引NULL 表示没用索引key_len用到的索引字节长度可以倒推哪些索引列被用上了rows优化器估算扫描行数估算值越大查询越危险Extra附加信息Using index覆盖索引、Using index condition索引条件下推、Using where回表后过滤、Using filesort文件排序等type 里的 ref 表示通过非唯一索引匹配到多行range 表示范围扫描。如果一条查询 type 能从 ALL 变成 ref通常意味着查询时间能有一个量级的改善。key_len 的计算是很多人的知识盲区。假设有一个(user_id, status)复合索引其中 user_id 是 BIGINTstatus 是 TINYINT。BIGINT 占 8 字节TINYINT 占 1 字节由于是可变长字段 VARCHAR 还需要加 2 字节长度前缀以及 NULL 标志位按需加字节。对定长字段来算key_len 大约等于“用到的所有索引列的字节长度之和”。如果 EXPLAIN 里 key_len 只显示 8说明只有 user_id 被用上了status 没参与过滤如果显示 9说明两个列都参与了。这个细节在判断复合索引是否真正“用满”时特别有用。4.2 一个真实订单查询的优化全过程为了把这几部分串起来我完整写一个案例。某天收到线上告警订单列表接口 P99 从 200ms 涨到 1200ms。对应慢 SQL 是这样的SELECT id, order_no, user_id, status, amount, create_time FROM orders WHERE user_id 823109 ORDER BY create_time DESC LIMIT 20;orders 表当时有三千多万行。初步看 WHERE 里有 user_id这个字段当时已经有一个单列索引idx_user_id为什么还会慢第一步先跑 EXPLAINEXPLAIN SELECT id, order_no, user_id, status, amount, create_time FROM orders WHERE user_id 823109 ORDER BY create_time DESC LIMIT 20;结果里 key 显示idx_user_id看起来走了索引但 type 是 refrows 估算约 5 万Extra 里出现了Using index condition; Using filesort。问题就出在“查到 5 万行然后 filesort 排序”。B 树索引只帮我们快速定位了 user_id 823109 的行但这些行在索引里并没有按 create_time 排序所以得到一个包含 5 万个主键的中间结果集后还需要再进行一次内存或磁盘排序最后取前 20 行。数据一多filesort 就成了瓶颈。改造思路很清晰把索引从(user_id)升级为(user_id, create_time)。这样同一个 user_id 下的数据在索引中已经天然按 create_time 排序MySQL 遍历索引时可以直接按顺序取出前 20 个主键再去聚簇索引回表拿完整行filesort 可以直接消除。ALTER TABLE orders DROP INDEX idx_user_id, ADD INDEX idx_user_create (user_id, create_time);改造后重新 EXPLAINExtra 里Using filesort消失rows 估算从 5 万降到几十接口 P99 回到 180ms 左右。这个案例很有代表性索引不是“建了就完事”列的顺序决定了它能解决什么问题。同样两个列(user_id, create_time)和(create_time, user_id)对业务的意义千差万别。设计索引时要把 WHERE 条件、ORDER BY、GROUP BY 全考虑进去。4.3 慢查询日志与索引审计如果已经上了生产环境才发现慢 SQL第一步不是到处加索引而是先把慢查询日志打开找到真实的“作案 SQL”。MySQL 里可以这样配置SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;long_query_time单位是秒1 表示超过 1 秒的记录到日志。生产中建议先用工具统计pt-query-digest可以聚合慢日志按总耗时排序直接告诉你哪些 SQL 消耗了最多时间比刷一堆原始日志高效得多。拿到 Top SQL 之后逐个EXPLAIN再统一规划索引避免出现“今天为 A 查询建一个索引明天为 B 查询又建一个索引”最后一张表挂了八九个索引写入越来越慢。用sys.schema_redundant_indexes可以查冗余索引长线维护视角要有。5. 索引对锁的影响从索引失效到死锁现场5.1 InnoDB 行锁为什么依赖索引InnoDB 的行锁不是“对哪行数据锁哪行”而是“通过索引找到记录后在索引记录上加锁”。这句话直接决定了索引失效的巨大风险。如果你执行UPDATE orders SET amount amount 1 WHERE user_id 823109而 user_id 恰好没有索引InnoDB 没法通过索引快速定位到要锁定的行只能扫描全表然后把扫描过程中访问到的每一行都加上锁。这在低并发下可能只是慢高并发场景下就是灾难大量事务互相等待对方持有行的锁阻塞成一大片。更值得警惕的是索引失效的时候 DELETE 也会锁大量行。我见过某团队因为给 VARCHAR 列传数字参数导致 delete 语句没走索引直接把整张表的行锁了个遍业务卡死十几分钟。所以前面讲的索引失效不只是性能问题更是并发安全和可用性问题。任何上生产环境的 DML 语句我都建议先跑一遍 EXPLAIN 确认至少能用上 range 或 ref 的索引再不济也得是 eq_ref绝不允许 ALL 级别的大范围 DML 直接上线。5.2 两个常见死锁案例解读死锁和索引的关系很多人没想清楚。InnoDB 加锁是有顺序的不同事务以不同顺序获取同一组资源时就可能互相等死。下面两个案例很典型。案例一同一张表两条 SQL 索引选择不同。事务 A 执行UPDATE orders SET status1 WHERE user_id100 AND create_time2025-01-01事务 B 执行UPDATE orders SET status2 WHERE order_no...。如果 A 用联合索引先锁定了某些主键B 用主键索引直接锁定另一些主键两者后续又互相访问到对方的主键区间就可能形成环形等待。规避办法是尽量让同一类操作在 SQL 层面统一查询条件并保持索引顺序一致。案例二复合索引引发的间隙锁竞争。在 REPEATABLE READ 隔离级别下InnoDB 的间隙锁除了锁定已存在的记录还会锁住记录之间的“间隙”。两个事务都在同一范围内插入新数据哪怕插入的是不同主键也可能互相阻塞因为它们的插入位置落在了同一个区间锁里。如果 UPDATE 或 DELETE 的索引条件范围过大锁定间隙自然扩大死锁概率直线上升。解决办法是尽量把 DML 条件收敛到能精确命中的索引区间范围越窄越好。排查死锁有一个现成命令SHOW ENGINE INNODB STATUS\G在输出的 LATEST DETECTED DEADLOCK 段落里可以直接看到两个事务持有哪些锁、等待哪些锁以及对应的 SQL 语句。看到锁记录上的索引名和主键值基本就能还原冲突现场。6. 面试常问的索引问题与速答清单这一章留给正准备面试的朋友。索引是 MySQL 面试的“兵家必争之地”我整理了几个高频问题每个附一句核心回答但你最好能结合自己的项目展开讲背答案反而坏事。为什么主键一般建议用自增 ID自增 ID 是顺序写入的新插入的主键值总比已有最大值大B 树的叶子节点基本只会向右追加页分裂概率低。如果用 UUID 或雪花这类随机性强的值做主键新值可能落在已有数据的中间位置频繁触发页分裂写放大明显二级索引占用的空间也会膨胀。业务需要唯一业务号时可以加一个唯一索引来兜底主键仍用自增。为什么联合索引要遵循最左前缀B 树索引本身是对“索引列元组”进行字典序排序的先按第一列排第一列相同再按第二列排。查询能利用索引的前提就是你的条件能和“这个元组的排序前缀”对齐。所以跳过第一列的条件本质上等于在一个无序的第二列上做查找排序结构帮不上忙。索引是不是越多越好不是。索引消耗磁盘空间拖累写入而且优化器在多个可选索引之间做选择也要花费额外成本。碰到查询慢先分析真实慢 SQL再精准补充索引比一把梭建满所有可能组合可靠得多。整数类型和字符串类型做主键有什么差别整数比较和排序效率更高占用的字节更少二级索引叶子节点存的主键值也更小能减小整个索引体积。能用整数主键尽量用整数。WHERE 条件里 IN 和 EXISTS 怎么选在 MySQL 优化器的实际行为里IN 多数情况下会转成半连接优化EXISTS 也很高效。关键是两边列都有索引且关联字段类型一致否则照样出问题。别迷信某一种写法EXPLAIN 才是最权威的。为什么说普通索引要尽量避免 NULL索引列允许 NULL 会增加复杂性。NULL 参与比较结果不确定统计信息也可能失真IS NOT NULL难走索引。表设计时能用默认值就不留 NULL这是低成本高收益的习惯。什么是 ICP索引条件下推Index Condition Pushdown是 MySQL 5.6 开始的一项优化在从二级索引读取记录时引擎直接把 WHERE 条件中能由索引列判断的部分下推到存储引擎层过滤减少回表次数。EXPLAIN 里 Extra 出现Using index condition就是触发了这个优化。写完这一节可以自查一下如果面试官问“你线上有没有遇到过索引失效”你能不能完整讲出“慢 SQL → EXPLAIN → 隐式转换 → 改 SQL → 效果对比”的完整链条能讲出这种细节比背一百个理论知识点都管用。最后分享一点个人体会。我经手过不少慢 SQL 优化最深的感受是索引设计没有银弹所有结论都要建立在真实执行计划上。你的数据分布、查询模式、写入频率都不同网上的“最佳实践”只能当起点不能当终点。拿到一个慢查询先看 EXPLAIN再结合索引基数和业务特征判断最后小步上线验证这一步一步走下来远比记住几个“不能”更有价值。另外线上大表动索引前一定先看锁等待别在业务高峰期踩 DDL 的坑。如果你现在正面对一张索引混乱的表不妨从慢查询日志开始一条一条理顺效果会比你想得更明显。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进