做后端开发这些年,凡是带数据库的项目,十有八九的慢查询最后都会落到同一个点上:索引该建没建,或者建了一堆但根本没真正生效。很多人一听到“MySQL 索引的使用”就以为是写几个 CREATE INDEX,可实际工作里,索引用得好不好,直接决定同一套 SQL 在百万级数据量下是秒回还是卡死。这篇文章不讲虚的,先带你理解索引的底层存储逻辑,再把联合索引、最左前缀、索引失效、排序加速这些高频问题掰开揉碎,最后用 EXPLAIN 完整演示一遍线上慢查询的排查过程。不管你是刚入门的 MySQL 新手,还是被索引坑过不止一次的同学,这十分钟都能换回不少实战经验。
我个人很反感那种背八股式的索引教程,上来就甩一堆“索引失效场景”,也不讲为什么。所以下面每一节都会先解释原理,再给实操结论,并附上我踩过的坑和现在还在用的排查习惯。
1. 索引到底在解决什么问题
1.1 没有索引时,MySQL 在干“全表扫描”这件事
先想明白一个最基础的问题:MySQL 在执行查询时,默认动作是“从第一行开始,把整张表的数据一块一块读出来,逐条匹配 WHERE 条件”。这个过程叫全表扫描。表里只有几千条数据时无所谓,但一旦到了百万级别,哪怕每条记录只有 100 字节,MySQL 也要读取几十上百 GB 的数据才能给你返回几个结果。生产环境里,这种 SQL 跑一次就能把 IO 拖垮,业务方还以为是数据库死锁了。
索引的本质就是给数据建立一个“可跳过的检索结构”,让 MySQL 不用遍历每一行,而是直接锁定一个很小的数据范围。这里有个最经典的类比:新华字典。如果所有汉字都按拼音顺序堆在一起,你找“博客的博”就得从第一页翻起,这就是全表扫描。而拼音检字表告诉你“bo”在第几页,这就是索引。字典不可能把每个字都重新抄一份,它只是额外维护了一份非常精简的“目录”,也就是我们说的索引表。
所以请记住第一句话:索引是用额外的存储空间和写入成本,换取查询时的效率。它不是白给的,也不是越多越好。
1.2 B+树:一层一层往下找,IO 次数少得可怜
MySQL 的 InnoDB 存储引擎选择的数据结构是 B+树,网上很多文章把它画得花里胡哨,但核心就三点:
第一,B+树是一个多路平衡树,节点会分裂和合并,树的高度通常只有 2 到 4 层。就算一张表有几千万行,只要索引键区分度足够,从根节点到叶子节点只需要几次磁盘 IO。
第二,真正存储数据的是最底层的叶子节点。InnoDB 的插入操作永远发生在叶子节点,并且叶子节点之间用指针相连,形成有序链表,这对范围查询和排序非常友好。你可以把它理解成“目录本身就带顺序”,所以走索引拿到的一批数据天然就是有序的。
第三,非叶子节点只存索引键和指针,不存完整行数据。这样一页 16KB 的 InnoDB 数据页能容纳成千上万个索引键,大大降低了树的高度,也就减少了磁盘读取次数。这也是 B+树相比二叉树、哈希索引的核心优势。
我见过不少同事把索引当成“万能加速器”,建了索引就说 SQL 一定快。其实不对,索引生效有一个前提:你要查的数据确实能通过这个树形结构“缩小范围”。如果查询条件写得太发散,MySQL 优化器评估后觉得走索引还没全表扫描划算,照样会放弃索引。
1.3 聚簇索引与二级索引:两种索引的存储差异
InnoDB 里默认主键索引是聚簇索引,叶子节点直接存放整行数据。也就是说,表数据本身就是按主键顺序组织的一棵 B+树。这就引出一个重要推论:InnoDB 表必须有主键,没有显式主键时,它会从非空唯一索引里挑一个当主键,再不行就生成一个隐藏的 rowid 作为主键。
而其他索引(普通索引、唯一索引、联合索引)都是二级索引,也叫辅助索引。二级索引的叶子节点并不存完整行数据,只存“索引列的值 + 对应的主键值”。所以走二级索引查询时,通常会发生一次“回表”:先用索引树定位到主键,再拿主键去聚簇索引里取整行数据。
简单说,聚簇索引查一次就能拿到整行,二级索引查两次。理解了这点,你就能理解为什么“查询不需要回表”的覆盖索引能让性能起飞。当索引里已经包含了所有要查的字段时,MySQL 连回表都省了,直接在索引树里返回结果,这就是 EXPLAIN 里的 Using index。
2. 索引分类与联合索引选型
2.1 主键索引、唯一索引、普通索引,到底有什么区别
日常建索引时,最绕不开的就是这三种类型。我直接列个表:
| 类型 | 是否允许重复 | 是否允许 NULL | 一个表可建数量 | 存储特点 |
|---|---|---|---|---|
| 主键索引 | 不允许 | 不允许 | 只能有 1 个 | 聚簇索引,叶子存整行 |
| 唯一索引 | 不允许 | 允许(可以有多个 NULL) | 可有多个 | 二级索引,叶子存主键 |
| 普通索引 | 允许 | 允许 | 可有多个 | 二级索引,叶子存主键 |
主键索引和唯一索引在查询性能上几乎没有差别,因为它们定位到一个不重复的值时都很快。真正的差别在约束和数据完整性上。主键用来唯一标识一行记录,所以非空且唯一;唯一索引只是约束“业务上不能重复”,比如用户表里的手机号字段,可以建唯一索引,但如果存在历史脏数据导致部分手机号为 NULL,依然能建成功。在 InnoDB 中,主键索引还承担物理组织的角色,二级索引叶子节点存的是主键值。如果主键是自增整数,插入永远追加在 B+树末尾,页分裂少;如果主键是随机的 UUID,插入时数据需要频繁移动,页分裂就会很严重,写入性能明显下降。这也是我不建议用 UUID 当主键的核心原因。
唯一索引还有一个容易忽略的坑:它对写入会做唯一性检查,所以插入和更新时要额外读一次索引做判重。普通索引没有这个检查,写入略快。仅从性能出发,业务并不要求唯一约束的字段不要顺手加 UNIQUE。
2.2 单列索引和联合索引怎么取舍
单列索引就是只用一个字段建索引,联合索引是用多个字段一起建索引。很多人习惯给每个出现在 WHERE 里的字段单独建一个索引,觉得这样最保险,这是典型错误。MySQL 在一个查询里虽然可以用多个单列索引做 index merge,但优化器要额外合并结果集,多数时候效果不如一个设计合理的联合索引。
联合索引的核心价值不是“多个单列索引的叠加”,而是让索引列之间配合扫描。比如索引 (a, b),MySQL 可以先按 a 锁定一个范围,再在这个范围内按 b 精确过滤。这一步叫索引下推,它把原本要在回表后用 WHERE 过滤的动作提前到了索引层,减少了回表次数。
所以取舍原则很明确:高频查询里有多个条件同时出现时,优先考虑联合索引;只在 WHERE 里单独出现的字段,才考虑单列索引。不要一上来就建一堆单列索引,后人维护起来崩溃,优化器也未必领情。
2.3 最左前缀原则:联合索引的“游戏规则”
联合索引 (a, b, c) 本质上先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。所以 MySQL 只能从最左边的 a 开始连续匹配。查询条件里如果完全不带 a,这个索引基本废掉;如果只带 a 和 c 而跳过 b,那么只有 a 能走索引,c 只能在索引返回的数据里做过滤,无法再利用树结构精确查找。
这个规则是新手最大的认知分水岭。“最左前缀”并不是说 SQL 的写法上 a 必须写在 b 前面,而是说查询条件里必须包含索引的最左列,并且各个条件的等值关系要与索引构建顺序匹配。比如索引是 (user_id, status, create_time),那么 WHERE status='paid' AND user_id=123 同样走索引,因为优化器会先取出 user_id=123 的区间,再在这个区间里挑 status。
最左前缀还牵涉到“范围列”问题。如果查询里出现了 status > 'paid' 这类范围条件,那 create_time 再放进去也是白搭,因为 B+树在一个范围区间内无法继续对后面的列做定位。换句话说,联合索引的“等值字段”可以一直复用,一旦遇到范围条件,后面的索引列就失效了。
2.4 经典问题:where 条件 a and b,索引应该怎么建
这是热搜里非常高频的问题,也是我之前在团队里讲过无数遍的场景。假设表结构大概是:
CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `status` tinyint NOT NULL, `create_time` datetime NOT NULL, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;业务里最常见的 SQL 是:
SELECT * FROM order_info WHERE user_id = 123 AND status = 1 ORDER BY create_time DESC LIMIT 20;这种“等值条件加排序”的组合,该怎么建联合索引?我的习惯分三步。
第一步,把所有等值条件字段找出来:user_id 和 status,这两个都是等值匹配,它们的先后顺序理论上不影响索引是否能命中,因为优化器会自己调整位置。但为了和未来扩展对齐,一般把区分度高的放前面,或者把经常单独查询的字段放前面。
第二步,看 ORDER BY 字段。如果期望避免 filesort,需要把 create_time 也加进索引,并且它的位置要放在等值字段之后。因为只有前面的字段都确定了等值,后面的字段才能利用 B+树的天然有序性完成排序。
第三步,得出推荐索引:联合索引 (user_id, status, create_time),或者 (status, user_id, create_time) 也可以,但要考虑是否另一套组合能覆盖更多查询。如果业务里还有单独的 WHERE user_id = ? 查询,那么 (user_id, status, create_time) 就是最优解,因为最左列单独也能用。如果单独按 status 查询也很高频,那 (status, user_id, create_time) 可能更好,但代价是 user_id 的单独查询没法走到联合索引。
一句话总结这个问题的答案:先把等值条件按区分度排序,再把排序字段放在等值条件的后面,最后用最左前缀校验当前索引能不能服务现有查询。不要迷信“哪个字段经常出现就放第一个”,要结合 ORDER BY、GROUP BY、范围查询和覆盖列综合判断。
3. 创建索引的实操方法与空间成本
3.1 建索引的三种姿势
我平时建索引用的方式大概就三种,各有各自的使用场景。
建表时直接定义,适合表结构还没上线时,一次性把主键、唯一键和普通索引都写清楚:
CREATE TABLE `test_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `mobile` varchar(20) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_mobile` (`mobile`), KEY `idx_created_at` (`created_at`) ) ENGINE=InnoDB;表已经存在,需要补充索引时,用 ALTER TABLE,适合加索引时可以指定算法和锁策略:
ALTER TABLE test_user ADD INDEX idx_email (email); ALTER TABLE test_user ADD UNIQUE INDEX uk_mobile (mobile);还有一种独立的 CREATE INDEX 语法,语法上等价于 ALTER TABLE ADD INDEX,但语义上更侧重“创建”,我习惯在脚本里用它,方便阅读:
CREATE INDEX idx_email ON test_user(email);删除索引时用 DROP INDEX,或者 ALTER TABLE DROP INDEX。注意,删除主键索引要谨慎,因为 InnoDB 主键不仅承担索引职责,还负责表的物理编排。
3.2 查看索引与执行计划
索引建完,如何确认它真的存在并且被使用?两条命令必不可少。
第一条是查看表上的索引信息:
SHOW INDEX FROM test_user;它会输出索引名、字段顺序、基数(Cardinality)、是否是唯一索引等信息。第二条是查看 SQL 的执行计划,也就是 EXPLAIN:
EXPLAIN SELECT * FROM test_user WHERE email = 'a@b.com';我最关注的是 type、key、rows、Extra 这四列。type 从好到差大致是 system、const、eq_ref、ref、range、index、ALL,看到 ALL 基本就是全表扫描了。key 表示实际用到的索引,rows 表示预估扫描的行数,Extra 里的 Using index 是最好情况,Using filesort 和 Using temporary 则说明排序或分组没走索引,需要优化。
需要注意的是,EXPLAIN 是预估结果,不是真实执行结果。分析热点问题时,我会再加一条 EXPLAIN ANALYZE(MySQL 8.0+)看真实耗时和实际扫描行数。
3.3 索引表空间:索引到底吃了多少磁盘
很多同学建索引时完全不算空间账,直到磁盘报警才开始查。在 InnoDB 中,索引数据和表数据都存放在表空间里。默认情况下,如果开启了独立表空间(innodb_file_per_table=ON),每个表对应一个 .ibd 文件,索引和表数据共用这个文件;如果用的是共享表空间,索引数据就混在共享表空间中,想释放也只能整体回收。
精确算索引占多大,可以查 information_schema 里的统计信息,但那个更新是采样式的,并不精确。最稳的办法是直接看操作系统层面的 ibd 文件大小:
ls -lh /var/lib/mysql/yourdb/test_user.ibd这个文件大小包含了所有索引和数据。想单独评估索引占比,可以通过索引键长度估算:一个二级索引记录大约等于“索引列长度 + 主键长度 + 一些头信息”。假设主键是 8 字节 bigint,email 是 varchar(100) 实际平均 30 字节,每条索引记录大约 50 字节左右,一页 16KB 大约能放 300 条,1000 万行数据就需要 3 万多页,将近 600MB,这还不算 B+树内部节点的开销。所以我常说,索引不是免费的午餐,建索引前先想想这条字段到底有多长。
另外,删除索引后,表空间并不会自动缩水,除非你用 OPTIMIZE TABLE 或者 ALTER TABLE ... FORCE 重建表。但这类操作在线上会锁表或者占用大量 IO,要避开业务高峰。
3.4 命名规范与维护纪律
索引命名这件事看似不重要,真到了排查问题的时候,一个不规范的索引名能把人气死。我一般用这么一套规则:
- 主键索引:PRIMARY,没得选
- 唯一索引:uk_字段名_字段名
- 普通索引:idx_字段名_字段名
- 联合索引:idx_字段名_字段名_字段名,字段名之间用下划线分隔
这套规则的好处是,看到索引名就知道索引覆盖了哪几个字段,不用每次 SHOW INDEX 去猜。另外一个维护纪律是:每条索引都要有存在的理由。我见过一个订单表建了十几个索引,其中好几个的字段前缀完全一致,只差一个尾部字段,这明显就是不同同学“各加各的”结果。索引越多,INSERT、UPDATE、DELETE 时维护成本越高,Buffer Pool 里被索引缓存占掉的内存也越多,最终影响整体性能。给已有表新增索引前,我会先用 sys.schema_unused_indexes 查一下哪些索引自打建好就没被用过,先把没用的清掉再谈新增。
4. 哪些场景会让索引失效
4.1 八种典型的失效场景
索引失效是面试高频,也是线上事故高发点。失效不等于删除索引,而是优化器评估后认为这个索引帮不上忙,选择了更笨的办法。我整理出八种最常见的情况,每一项都直接给坑和解决办法。
第一,对索引列使用函数。比如 WHERE DATE(create_time) = '2024-06-01',MySQL 对 create_time 套了一层函数后,B+树的有序性就被打破了,只能全表扫描。解决办法是先算好范围:WHERE create_time >= '2024-06-01 00:00:00' AND create_time < '2024-06-02 00:00:00'。
第二,隐式类型转换。如果 phone 字段是 varchar,查询写 WHERE phone = 13800138000,MySQL 会把字符串列转换为数字,导致索引失效。正确写法是给数字加引号。反过来也一样,数字列和字符串比较也可能出问题。
第三,LIKE 前置通配符。WHERE name LIKE '%张',因为开头不确定,B+树无法定位起点,索引直接失效。解决办法是查线上数据时尽量避免这种写法,必要的话考虑全文索引或搜索引擎类方案。后匹配的 name LIKE '张%' 不受影响。
第四,OR 条件里有非索引列。WHERE id = 1 OR name = 'abc',即使 id 有索引,name 没索引,MySQL 也没法用两个区间直接合并,干脆全表扫描。解决办法是把 OR 两边都改成有索引的字段,或者拆成两条 SQL 用 UNION ALL 合并。
第五,联合索引不满足最左前缀。索引是 (a, b, c),查询只写了 b = 1,索引从第一条路就断了。
第六,范围条件后面的索引列。WHERE a = 1 AND b > 5 AND c = 3,索引 (a, b, c) 最多用到 a 和 b,c 只能回表过滤。所以联合索引字段顺序要仔细排:等值字段放前,范围字段放后,如果范围字段本身不是核心条件,甚至可以不放进去。
第七,使用 != 或 <>。这个要分情况,如果表的区分度很高并且访问的数据量很小,优化器偶尔会走索引;但很多人期望它每次走索引,实际却变成全表扫描。区分度低的字段,比如 status 只有 0、1、2 三个值,无论怎么建索引,优化器都会选择扫描全表,因为走索引的代价反而更高。
第八,IS NULL 和 IS NOT NULL。InnoDB 二级索引对 NULL 的处理比较特殊,普通索引中 NULL 值可以被多个记录使用,但当 WHERE name IS NULL 时,不是所有情况都能用到索引。如果确实经常要查“某字段为空”,可以考虑给该字段设定默认空字符串或 0,再建普通索引。
4.2 用 EXPLAIN 定位索引失效
遇到慢查询,我的第一反应就是跑 EXPLAIN,而不是靠肉眼猜。给你看一个实际例子。假设订单表有一个索引 idx_user_status(user_id, status),现在执行:
EXPLAIN SELECT * FROM order_info WHERE status = 1 AND user_id = 123;结果里 type 为 ref,key 为 idx_user_status,也就是走了索引。但如果把条件改成:
EXPLAIN SELECT * FROM order_info WHERE status = 1;因为 WHERE 没包含最左列 user_id,索引失效,这时 type 会变成 ALL,rows 直接变成全表行数。这就是最左前缀的验证现场。
排查索引失效时,我习惯把 SQL 里的条件逐个用 EXPLAIN 跑一遍,每加一个条件看一次执行计划。通过改变条件,观察 key 和 rows 的变化,就能精确找出是哪个字段“打断”了索引链路。这种排查法比我对着索引定义脑补要快得多。
4.3 一个线上慢查询的修复过程
有次线上报警,某个列表接口响应从 50ms 涨到了 5 秒,查慢查询日志发现典型 SQL:
SELECT id, user_id, title, create_time FROM article WHERE category_id = 10 ORDER BY create_time DESC LIMIT 20;表里当时的索引是单列索引 idx_category(category_id)。EXPLAIN 一看,type 是 ref,key 也用了 idx_category,但 Extra 里出现了 Using filesort。也就是说,系统先用 category_id 筛出了几十万行,再对 create_time 做了一次磁盘级排序,最后才取 20 条,慢得理所当然。
修复方案就是把排序字段也收进索引:
ALTER TABLE article ADD INDEX idx_category_create_time (category_id, create_time DESC);MySQL 8.0 里 DESC 后缀能直接建降序索引,8.0 之前的版本加不加 DESC 其实没有影响,因为旧版本无法按降序存储,优化器会反向扫描。这个索引一加,EXPLAIN 的 Extra 里 Using filesort 消失了,变成了 Using index condition 和 Using index。接口耗时直接降回 60ms 左右。
这个案例给我最大的教训不是“建个联合索引就行”,而是“发现问题时先看 Extra 列的 filesort 提示,再反推索引字段顺序”。
5. 索引对排序和事务的影响
5.1 用索引干掉 filesort
MySQL 的排序分两种:一种是直接从索引里按顺序读取,根本不需要额外的排序动作;另一种是因为查询走不上索引顺序,MySQL 只能把结果先装载到内存或磁盘上排序,也就是 filesort。filesort 不一定真的落到磁盘,但即使只在内存中排,也是白消耗 CPU 和临时空间。
从执行计划里可以直观判断:Extra 里出现 Using filesort,基本就意味着排序没有利用上索引。我见过很多面试题问“MySQL 里怎么优化 ORDER BY”,答案绝不是“排序本身很快”,而是“让排序字段和 WHERE 条件字段组成联合索引,并满足最左前缀”。比如 WHERE a = 1 ORDER BY b,如果索引是 (a, b),那么 MySQL 在 a=1 这个区间里读到的数据就已经按 b 排好了,LIMIT 20 只需要取前 20 条就结束,效率极高。
这里要强调一点:DESC 索引并不是万能的,只有热点查询里明确要求降序,并且与 ORDER BY 的方向完全一致,才需要建 DESC 索引。MySQL 8.0 之前所有索引都是升序存储,查降序时优化器反向扫描即可,性能也不差。8.0 之后如果确认反向扫描有压力,显式指定 DESC 是更可控的做法。
5.2 索引、锁与事务隔离:索引失效不只是变慢那么简单
索引失效在事务场景下还有一个容易被忽略的连锁反应:锁范围失控。
InnoDB 在可重复读隔离级别下有间隙锁机制,目的是防止幻读。如果 UPDATE ... WHERE 条件的字段没有索引,InnoDB 只能全表扫描定位要更新的行,这意味着它可能需要给整张表的所有记录和间隙加锁。虽然 MySQL 有一定优化会减少锁的数量,但风险非常大。而有索引时,锁直接落在索引命中的记录和对应间隙上,范围精确得多。
我之前处理过一个故障:某运营后台按用户状态批量更新一条数据,因为那个状态字段没有索引,执行时不少其他请求被堵在锁等待上。当时的解决办法并不是改事务逻辑,而是给状态字段补了一个普通索引,更新 SQL 本身没动,锁竞争立即缓解。所以处理事务型慢查询时,别只盯着查询耗时,也要想想“这条 SQL 会锁住多少行”。索引越不精确,锁的波及面越大,甚至可能引发连锁死锁。
事务与索引之间的第二个影响是 MVCC。二级索引上的老版本记录通过 undo log 维护,聚簇索引里保留了隐藏的事务 ID 和回滚指针。查询走索引可以减少扫描的可见性判断成本,这也是为什么高频事务表更需要把索引设计做扎实。
5.3 主键索引和唯一索引的区别,面试和工作都要会用
主键索引和唯一索引是 MySQL 面试中出现频率最高的两个概念,这里把它们的区别说得再透一点。
从约束层面看,主键索引不可以为 NULL,且一张表只能有一个;唯一索引可以为 NULL,也允许存在多个 NULL 值,因为 InnoDB 对唯一索引的 NULL 判定是“NULL 不等于 NULL”。从存储层面看,InnoDB 中主键索引是聚簇索引,叶子节点存储整行数据;唯一索引是二级索引,叶子节点存储索引列和主键值。这就意味着,直接通过主键回表是零成本,而通过唯一索引查询还需要一次回表操作。
从写入性能看,主键自增是最高效的,因为新记录总追加在 B+树右侧,页分裂极少;而 UUID 主键会导致 B+树中间频繁分裂。唯一索引的写入则多一步唯一性校验,普通索引没有这一步,所以如果某个字段只是业务上需要快速检索,并不需要唯一约束,建普通索引即可,没必要硬上 UNIQUE。
还有一点非常关键:InnoDB 中二级索引必然包含主键值,所以主键越短,所有二级索引的叶子节点就越小,占用的表空间和 IO 就越少。这也是我反复提醒团队“不要把超长字符串当主键”的根本原因。
6. 索引调优与常见问题速查
6.1 冗余索引识别与清理
索引调优第一步不是增加索引,而是清理冗余索引。什么叫冗余?最典型的是已经有联合索引 (a, b),同时又建了单列索引 (a)。因为联合索引的最左列就是 a,单列索引 (a) 完全能被 (a, b) 替代,除非 (a) 还能独立支撑另一类查询且联合索引的字段顺序会导致排序不符合要求。
另一种容易混淆的情况:已有 (a, b),又建了 (b, a),这两个并不冗余,因为它们的最左列不同,服务的是不同查询需求。已有 (a, b) 和 (a, b, c),则后者在多数场景下可以替代前者,因为 (a, b, c) 覆盖了 (a, b) 的能力,还多了 c。
清理冗余索引时,MySQL 8.0 可以直接查 sys 库:
SELECT * FROM sys.schema_redundant_indexes;MySQL 5.7 也有该方法,但部分版本需要初始化 sys 库。删除索引前,务必确认没有业务 SQL 依赖它,最稳妥的是先在测试环境把旧索引隐藏(INVISIBLE)观察一段时间,确认无影响后再 DROP。
6.2 索引相关的常见问题速查表
这些年我在社区和团队里收集了不少索引相关的典型问题,整理成一张速查表,方便各位直接对照:
| 现象 | 可能原因 | 快速排查 |
|---|---|---|
| 查询突然变慢几十倍 | 数据量增长导致扫描行数上升 | EXPLAIN 看 rows 和 type |
| 明明建了索引但没走 | 最左前缀不满足 / 隐式转换 / LIKE 前置通配 | 检查 WHERE 条件是否触碰索引列 |
| Extra 出现 Using filesort | 排序字段不在索引中 | 重建联合索引把排序字段加进去 |
| Extra 出现 Using temporary | GROUP BY 字段顺序与索引不一致 | 调整索引字段或 SQL 逻辑 |
| 更新语句锁等待严重 | WHERE 条件没有索引,导致锁范围过大 | 给 WHERE 字段加索引 |
| 索引太多导致写入慢 | 二级索引数量过多,写一条索引要维护多次 | 清理冗余索引,合并联合索引 |
| 表空间暴涨 | 二级索引过大或历史版本数据累积 | 查看 ibd 文件大小,评估索引必要性 |
| 查询返回大量重复索引 | 覆盖索引没设计好 | 尝试用覆盖索引减少回表 |
顺带提一句,像“MySQL SSL 连接错误”“Docker 安装 MySQL 失败”这类问题,基本和索引没有直接关系,排查时先查网络层、账号权限和配置文件,不要一上来就往索引上套。
6.3 关于索引数量和维护成本的一点经验
索引的维护成本平时不容易量化,但一旦遇到大批量数据导入或高并发写入,就会立刻暴露。每插入一行,InnoDB 除了修改聚簇索引,还要同步修改该表所有二级索引。五张二级索引就是五条索引链的写入,任何一条索引里的页分裂都可能拖慢整体插入速度。
所以我的经验是:单表索引数量尽量控制在 5 个以内,核心高频写表的索引更是要反复审视。尤其不要出现“同一个前缀字段拆成三四个索引”的奇观。如果要支持复杂的多条件筛选,优先考虑用联合索引覆盖最核心的两三种查询组合,剩下低频需求就让全表扫描自己扛,或者交给统计报表临时处理。
索引调优也不是一次性的工作。业务数据量在涨,索引基数在变,优化器的判断依据也在变。我每个月会花小半天时间,把慢查询日志里的 Top SQL 拉出来,重新用 EXPLAIN 过一遍,专门检查有没有索引没“跟上数据量增长”。这比临时抱佛脚地加索引要省心得多。
回到开头那个问题:where 条件 a and b 应该怎么建索引。你已经知道答案了:先看等值条件,再看排序字段,最后用最左前缀校验。但比这个具体答案更重要的,是掌握背后的原理。我自己刚接触 MySQL 时,也以为索引是“银弹”,后来被线上故障反复教育,才慢慢明白索引其实是一笔带利息的投资。你会用它,它会帮你省下成百上千倍的时间;你不会用它,它就悄悄吃掉磁盘、拖慢写入、扩大锁范围。这篇内容就是一个完整的排查工具箱,以后你再碰到慢查询,不妨从索引开始查起。