说实话,我最早搞数据库的时候,对索引失效这件事特别迷信。面试题里背了一堆"口诀"——什么左模糊失效、函数失效、or失效、隐式转换失效……背得滚瓜烂熟,可一到线上排查慢查询,照着口诀逐一排除,死活定位不到真凶,最后DBA老大瞟了一眼慢日志说:"这列你建索引了吗?"我当场愣住,那种尴尬,干过开发的人应该都懂。
后来踩的坑多了,慢慢才反应过来:索引失效的本质不是"口诀没背全",而是没有理解数据库优化器是怎么思考的。它不关心你"觉得"应该走索引,它只算哪条路"性价比更高";如果一条SQL的WHERE条件在优化器眼里根本无法利用B+树的有序结构去快速定位,它宁可选择全表扫描。所谓索引失效,绝大多数不是索引"坏了",而是查询条件和索引结构之间出现了无法跨越的鸿沟。
这篇就把我在生产环境里遇到过的、能系统性归类索引失效场景的内容,一次讲透。你可以把它当成一份排查手册,遇到慢SQL时对照着检查,比瞎猜强得多。适合使用MySQL为主的开发同学,也适合刚接触执行计划的初学者。
1. 优化器视角下的索引失效本质:B+树有序性是怎么被破坏的
要搞清楚失效,先得搞清楚索引为什么快。MySQL的InnoDB索引底层是B+树,叶子节点按索引列的值有序排列。所谓的"走索引",本质上是利用这种有序性,从根节点出发,逐层二分查找,把目标数据的定位范围从全表缩小到几条、几十条。所以,B+树的有序性是这个机制唯一的命脉。
一旦查询条件让优化器无法利用这个有序性,索引就成了摆设。我用一个非常生活化的例子来解释——查字典。
假设你手里有一本按拼音排序的《新华字典》:
- 场景A:你要查"张"字。你会怎么查?直接翻到"zhang"对应的大致页码,然后逐页逼近。这就是走索引,利用的是字典整体的拼音有序性。
- 场景B:你要求"把拼音首字母是z、且最后一个字母是g的字都找出来"。你能直接翻到某一页吗?不能。"z*g"这个条件在拼音排序的字典里是分散的,你得从z开头那一大段里挨个翻。这就是索引失效(前缀匹配失效)。
- 场景C:字典是按拼音排的,你却想按偏旁部首找字。排序规则根本不支持这个查法,这就是索引结构无法满足查询需求。
所以你看,索引失效背后有一个统一的底层逻辑:WHERE条件中,索引列的有序性被破坏,或者WHERE条件与索引的有序性不匹配。所有的失效场景,都是这个"有序性破坏"的具体表现形式。
下面这张表格整理了我认为最核心的失效分类,先建立宏观认知,后面逐一展开:
| 失效大类 | 核心原因 | 典型场景 |
|---|---|---|
| 对索引列施加额外操作 | 索引列参与函数、运算后,值被改变,B+树原有的有序值不匹配 | where id + 1 = 10、where DATE(create_time) = '2024-01-01' |
| 隐式类型/字符集转换 | 列值被强制转换后比较,与索引中存储的原始值不再一致 | where phone = 13800138000(phone是varchar) |
| 前导模糊查询 | '%'开头的LIKE无法定位起始扫描点,B+树有序性无法利用 | where name like '%张三%' |
| OR条件存在非索引列 | 优化器无法保证每个分支都走索引,只能全表扫描兜底 | where id = 1 or name = '张三'(name无索引) |
| 范围查询后的列 | 联合索引在当前列范围扫描后,后续列无法保证全局有序 | where a = 'x' and b > 100 and c = 'y'(索引abc) |
| 联合索引缺失前导列 | WHERE没有从联合索引最左列开始,索引完全无法匹配 | 索引(a,b,c),条件只有 where b=1 |
| 统计信息严重失真 | 优化器依据错误的基数估算,误判全表扫描更快 | ANALYZE TABLE长期未执行 |
这一张表就是整篇文章的骨架,接下来每一个场景,我都会结合真实生产问题,把为什么失效和怎么绕开讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 对索引列"动手脚":函数、运算与隐式转换的三连坑
2.1 函数操作:优化器不是算不出来,是划不来
在索引列上使用函数,是最经典的失效场景。数据量小的时候感觉不到,数据量一上来,慢查询日志里最先出现的往往就是这种。
举一个我接手过的真实案例。订单表有上千万条数据,业务方习惯写:
mysql复制SELECT * FROM order_info WHERE DATE(create_time) = '2024-06-01';
逻辑上没毛病——查某一天的全部订单。但坏就坏在DATE(create_time)这个函数上。create_time列上的索引存储的是2024-06-01 10:23:45这种完整的时间戳值,B+树按照这个完整值排序。你让优化器查"所有DATE转换后等于'2024-06-01'的行",它得先把索引列每一行的值都做一次DATE()运算,然后才能判断等不等于。这个计算过程一旦套在全量数据上,索引的有序性就不存在了——因为"DATE()之后相等的值"在原始索引中是分散的,无法通过B+树的二分定位。
优化器经过成本估算后发现,走索引的代价是把全索引的值都函数化一遍再比较,和全表扫描差不多甚至更贵,于是直接放弃索引扫描。
正确的写法是把函数从索引列上"剥"掉,等价转换成范围查询:
mysql复制SELECT * FROM order_info
WHERE create_time >= '2024-06-01 00:00:00'
AND create_time < '2024-06-02 00:00:00';
这样create_time保持原始值参与比较,B+树就能通过二分直接定位到6月1日零点的位置,然后顺序扫描一整天。实测改造后,这个SQL的执行时间从3.2秒降到了80毫秒,差了40倍。
注意:像
DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-06-01'这类写法,同样失效。只要索引列被任意函数包裹,都要想办法转换成语义等价的范围查询。
2.2 隐式类型转换:varchar列与数字比较的隐形炸弹
这是一个比函数操作更隐蔽的坑,因为SQL看起来"没毛病"。
用户表里phone字段定义是varchar(20),存的是"13800138000"。某个开发同学写:
mysql复制SELECT * FROM user WHERE phone = 13800138000;
MySQL的优化器有一条规则:当比较的双方类型不一致时,隐式转换的优先级是数字优先于字符串。也就是说,它会尝试把varchar类型的phone列转换成数字,再与右边的数字比较。这就等于把上面的函数操作又演了一遍——索引列被"隐式的CAST"包了一层,B+树上的原始字符串值无法直接参与比较,索引失效。
这个场景的诡异之处在于:它不一定每次都失效。如果user表数据量很小,优化器哪怕全表扫描也无所谓,执行计划里看不到明显异常;但表一上千万行,这条SQL立刻原形毕露。
判断一种写法是否会触发隐式转换,有一个非常实用的经验规则:
varchar列 与数字比较 → 列被隐式CAST为数字 → 索引失效int列 与数字字符串比较 → MySQL能安全地将字符串转换为数字,且不会在int列上做转换 → 索引基本可用varchar列 与字符串比较 → 类型一致,正常走索引
为了避免这类问题,最稳妥的办法是应用层传入参数时保持字段的原始类型。phone是varchar,传参就传字符串;id是int,传参就传数字。我在代码评审时看到类似WHERE id = #{param}的写法,都会习惯性确认param是什么类型。
2.3 字符集不一致:跨表关联时隐式转换的另一副面孔
这个是连很多资深开发都会忽略的。两张表关联,A表的user_id是utf8mb4,B表的user_id是utf8,它们的排序规则(collation)不同。
mysql复制SELECT * FROM a JOIN b ON a.user_id = b.user_id;
MySQL在比较两个不同字符集的列时,会选择字符集优先级更高的一方对另一方做隐式转换。utf8mb4比utf8优先级高,所以B表的user_id会被强制转换为utf8mb4再比较。如果B表的user_id上有索引,这个索引就失效了——因为优化器需要在B表的索引值上套一层转换函数再做比较。
我在一次慢查询治理项目中就遇到过这种情况。某张从老库迁移过来的表字符集还是utf8,新表是utf8mb4,两张表做JOIN,驱动表有10万行,被驱动表有500万行,结果连接查询跑了将近15秒一直出不来。DBA排查后发现,被驱动表的索引列在转换后完全失效,优化器只能逐行全表扫描。
解决方案:要么把老表的字符集统一改成utf8mb4,执行ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4;要么在JOIN条件里手动指定转换:
mysql复制SELECT * FROM a JOIN b ON a.user_id = CONVERT(b.user_id USING utf8mb4);
但注意手动转换后的索引依然失效,所以这个方案只是权宜之计,还得从源头统一字符集。建表之初就统一使用utf8mb4,是规避这一类问题成本最低的方式。
2.4 算术运算:索引列参与加减乘除也是同款死法
索引列上做算术运算,和函数操作是同一类问题,放在一起说。
mysql复制SELECT * FROM account WHERE balance - 500 > 10000;
balance列参与减法运算后,B+树里存储的原始值已经不能直接与10000比较了,索引失效。正确写法是把运算移到等号/不等号的另一边,保持索引列"干净":
mysql复制SELECT * FROM account WHERE balance > 10500;
这不仅仅是为了索引,从语义上也是等价的。这个规则在做范围查询、翻页统计时特别容易踩到,比如按"创建时间减去多少天大于某日期"这类写法,务必把时间计算挪到常量一侧。
3. LIKE的前导通配符、OR条件与联合索引的三大"结构性失效"
3.1 前导模糊查询:'%关键字%'为什么必然失效
熟悉B+树查找机制后,前导模糊查询失效就很容易理解了。B+树叶子节点有序,走索引的前提是能确定一个起始扫描点。
WHERE name LIKE '张%':优化器知道从"张"开头的第一个索引值开始扫,能走到B+树定位,索引可用。WHERE name LIKE '%张%':第一个字符是通配符,优化器无法确定从哪个索引值开始找。理论上可以扫描整棵索引树再逐行判断,但那样还不如直接全表扫描+内存过滤,所以优化器选择放弃索引。
这里我有一个很反直觉的实测发现:在某些情况下,WHERE name LIKE '%张%'依然可能出现在执行计划的possible_keys里,但实际扫描行数却是全表行数,type列是ALL。这个现象容易误导人——你以为它"有机会走索引",实际上根本没有。所以分析执行计划时,重点关注type列(ref/range/eq_ref才说明真正用上了索引),而不是possible_keys。
改造思路(按优先级排序):
- 如果只需要判断是否包含某子串,可以考虑
INSTR(name, '张') > 0,但这同样走不了索引,适合小表。 - 如果业务确实需要"前模糊"搜索,且数据量很大,引入搜索引擎(ES)或者用覆盖索引+内存过滤的方案。
- 如果是后模糊的需求,
WHERE name LIKE '张%'就能正常走索引;这种场景在日常的搜索框热词匹配中利用率最高。
3.2 OR条件:只要有分支不走索引,全表扫描兜底
OR导致的索引失效,很多人的理解是"OR的字段必须都有索引",这个说法并不准确。准确的逻辑是:OR的每个分支,优化器都要能找到一个可用的索引路径,否则它无法只靠索引合并或分区扫描完成整个查询,就只能保守地全表扫描。
举个例子:
mysql复制SELECT * FROM user WHERE id = 1001 OR nickname = '张三';
id主键有索引,nickname没索引。优化器的思路是:走id索引找到1001,走全表扫找到所有人里nickname等于'张三'的,然后做合并。这样一来,整个查询的成本约等于全表扫描的成本,那还不如直接全表扫。所以执行计划直接选了ALL。
两种解法的取舍:
- 解法1:给
nickname也建上索引。MySQL 5.6之后支持Index Merge,两个索引分别扫描再合并结果,两个分支都能高效定位。但要注意,索引合并不是万灵药,分支多、结果集大时性能仍可能不理想。 - 解法2(更推荐):把OR改写为
UNION ALL或UNION:
mysql复制SELECT * FROM user WHERE id = 1001
UNION ALL
SELECT * FROM user WHERE nickname = '张三' AND id != 1001;
这样两个分支独立优化,各自走各自的索引,最后把结果拼起来。实测在某些复杂场景下,改写后速度能提升一个数量级。
3.3 联合索引的"最左前缀"失效:范围条件如何切断后续列的有序性
联合索引是最容易出"隐性失效"的地方,因为很多时候执行计划显示的key确实是这个联合索引,但实际使用效果却大打折扣。这就牵扯到一个关键概念:联合索引的有序性是分层的。
假设有联合索引(a, b, c),B+树先按a排序,a相同的再按b排序,b相同的再按c排序。
WHERE a = 1 AND b = 2 AND c = 3:完美匹配三层,全部走索引。WHERE a = 1 AND c = 3:只用了a这一层索引(ref),c的条件无法在索引层过滤,需要回表后再过滤。WHERE a = 1 AND b > 100 AND c = 3:这一条是最多人掉坑的。
第三个例子我要展开说说。a = 1等值匹配,然后b > 100是一个范围条件,在a=1的情况下,B+树中所有的b>100的行在叶子节点上是连续的,所以能用索引找到这一片。问题在c = 3:在b > 100这个范围之内,c的值是没有全局顺序的——联合索引只保证了同一个b值内c有序,不保证"所有b>100的行按c有序"。所以优化器只能把这个范围内所有行回表,再逐行过滤c=3。
这个现象叫做**"范围条件右侧断裂"**——一旦某列出现范围条件(>、<、>=、<=、BETWEEN),它右侧的索引列全部失效。这个"失效"不是说索引没用,而是说范围条件之后的列没法像等值条件那样完成精确的索引定位。
怎么破:遇到这种SQL,最常用的办法是调整联合索引的列顺序,把最常用作等值过滤的列放在前面,范围条件列尽量放后面。比如上述SQL如果改成联合索引(a, c, b),那么a=1等值定位后,c=3还能再精确过滤一层,最后在c=3的结果集里做b > 100的范围扫描,过滤效率完全不一样。
不过要注意:列顺序的调整要综合观察高频SQL模式,不能为了这一条SQL牺牲另外一大堆SQL。如果某个高频SQL里b的等值查询也很多,就要权衡。
3.4 联合索引缺失前导列:优化器想用也用不上
前面讲联合索引的有序性是分层的,这意味着:如果WHERE条件里没有联合索引的最左列,那么这个索引对优化器来说就等于没有"入口"。
假设索引是(user_id, status, create_time),某条查询是:
mysql复制SELECT * FROM order_info WHERE status = 1 AND create_time > '2024-01-01';
status没有索引前面顶着的user_id等于多少,B+树里所有status=1的行是分散在整个索引中的——因为索引先按user_id排序,再按status排序。优化器如果要利用这个索引,依然得扫全索引,不如全表扫。所以possible_keys里可能带这个索引名,但实际类型是ALL。
我见过很多"按多条件建索引"然后四五个查询用同一个索引的情况,往往就是这个下场——覆盖了某几个SQL,却把另外几个SQL的索引彻底废掉。遇到这类情况,核心解法不是再建一个大而全的联合索引,而是拆分需求,针对不同查询模式建不同的索引,或者在前端查询路径里强制带上user_id、tenant_id这类"天然前缀"条件。
4. 优化器的"主观判断"失效:统计信息、排序与回表成本
4.1 统计信息失真:真失效还是假失效?
有一种情况,明明SQL写得没问题,索引建的也没问题,但优化器就是不走索引。这时候要考虑:统计信息是不是过期了。
InnoDB通过采样统计来估算某个值在索引中的分布(基数,Cardinality)。如果表的增删改非常频繁,而统计信息没有及时更新——MySQL的ANALYZE TABLE不是每次DML都自动触发,而是看innodb_stats_auto_recalc的设置和变更行数比例——那么优化器拿到的可能是一份过期的"地图"。
举个例子,某业务表在凌晨批量导入数据后,紧接着白天有人用WHERE user_id = ? AND status = ?做查询,但status字段的分布已经和统计信息里的完全不一样了。优化器估算"这个status值可能过滤后还剩90%的行",觉得回表成本太高,于是选择全表扫描。
解决办法不是让开发改SQL,而是让DBA执行一次:
mysql复制ANALYZE TABLE order_info;
刷新统计信息后,优化器可能立刻回心转意。判断是否是这类问题,可以查看执行计划里预估行数(rows列)与实际返回行数差异是否巨大。如果预估10万行、实际只有100行,大概率是统计信息老化了。
4.2 回表成本过高:索引选择性的权衡
另一个优化的"主观判断",是回表成本。B+树分主键索引和二级索引:二级索引的叶子节点存的是主键值,通过二级索引找到主键后还要再回主键索引查一次完整行。如果某二级索引的选择性很弱——比如status字段只有"已支付、未支付、已取消"三四个取值——优化器一算:走这个二级索引扫半天,还要回表,和直接全表扫描没区别,干脆全表扫。
这种场景不算严格意义上的"索引失效",因为它本质上不是无法利用索引,而是性价比太低导致优化器不选。但它在慢查询里表现得和失效一模一样,所以排查时要留个心眼。
对策:
- 如果列基数太低,建单列索引帮助不大,考虑与其他高频过滤列组合成联合索引。
- 如果查询只需要某几个列,可以建覆盖索引,让二级索引包住查询的所有列,不需要回表,优化器就愿意用了。比如
SELECT count(*) FROM order WHERE status = 1,可以直接建(status, id)索引,count查询在索引树里完成。 - 对于低基数列,还可以引入额外的过滤条件或业务分区逻辑,从源头减少行数。
4.3 ORDER BY排序与GROUP BY分组:文件排序对索引的隐性破坏
排序/分组导致的索引失效,和WHERE条件的失效逻辑不太一样,但同样常见。很多人不知道:当SQL里同时有WHERE和ORDER BY时,优化器很可能为了排序而放弃本来可用的索引。
看一个例子:
mysql复制SELECT * FROM order_info
WHERE user_id = 1001
ORDER BY create_time DESC;
如果有一个(user_id, create_time)联合索引,这个SQL会非常爽:先通过user_id=1001定位到第一批叶子节点,然后因为索引内部已经按create_time有序,直接倒序遍历叶子节点就行,不需要额外排序。
但如果你的索引只是(user_id)单列索引,那查询流程就是:先通过user_id索引找到所有该用户的行,回表,再在内存中对create_time做排序(filesort)。当这个用户的行数不多时还好;行数一多,filesort就会成为性能瓶颈。优化器在计算成本时可能发现"走索引回表+排序"还不如"全表扫描+排序",于是索引再次被弃用。
GROUP BY同理。GROUP BY本质上也是先排序再分组,如果分组列不在索引的合适位置,MySQL就需要走临时表和文件排序。
优化策略:
- 尽量让ORDER BY/GROUP BY的字段包含在联合索引中,且和WHERE的等值条件组合后能形成"左前缀有序"。
- 如果排序方向与索引的默认方向不一致(比如索引是ASC,你要DESC),MySQL 8.0开始支持降序索引,可以针对方向建索引。
- 不要轻易对大表的非索引列排序,必要时用冗余字段或汇总表来解决。
5. 线上慢SQL排查:一条具体的失效定位链路
5.1 从慢查询日志到执行计划,怎么一步步定位失效根因
说了这么多场景,实际线上排查怎么落地?我把自己经常走的排查链路完整写出来,照着做基本能覆盖90%的情况。
第一步:从慢查询日志捞出目标SQL。使用mysqldumpslow或直接查询performance_schema都可以。重点记录三个信息:SQL文本、执行时间、扫描行数。
第二步:用EXPLAIN看执行计划。这是定位索引问题的核心工具。
mysql复制EXPLAIN SELECT * FROM order_info
WHERE user_id = 1001 AND status = 1
ORDER BY create_time DESC\G;
重点看这几个字段:
type:从好到差依次是system > const > eq_ref > ref > range > index > ALL。看到ALL,基本就确定是全表扫描。key:实际使用的索引名。如果为NULL,说明没走任何索引。rows:预估扫描行数。Extra:出现Using filesort表示有额外排序;Using temporary表示有临时表;Using where表示索引过滤后回表再过滤。
第三步:用 EXPLAIN ANALYZE(MySQL 8.0+)看实际耗时分布。这个命令会真实执行SQL并返回每个步骤的实际耗时和行数。比如它可以明确告诉你,某一步的预估行数(10000行)和实际行数(100行)相差巨大,从而怀疑统计信息问题。
第四步:按"索引列是否被动手脚→联合索引顺序是否合理→优化器成本估算是否异常"的顺序排查。这三板斧下去,大部分问题都能定位到根因。
5.2 一次典型的"索引失效"实地复盘:从3秒慢查询到80毫秒
分享一个我印象深刻的案例,完整复盘整个排查过程。
现象:某后台管理系统的列表页,用户反馈打开要等好几秒。慢查询日志里有一条SQL每天出现几百次:
mysql复制SELECT * FROM operation_log
WHERE DATE(operated_at) = '2024-05-20'
AND operator_id = 88
ORDER BY operated_at DESC
LIMIT 20;
operation_log有800万行,表上有idx_operator_time(operator_id, operated_at)联合索引。
当时我第一眼扫过去,直觉就是DATE(operated_at)引起的函数问题。但紧接着注意到另一个细节:即使去掉DATE函数,operator_id = 88 AND operated_at = ...范围查询 + ORDER BY operated_at DESC,这个联合索引顺序是(operator_id, operated_at),其实刚好是匹配的。
于是问题的根因就集中在DATE(operated_at) = '2024-05-20'上。我对这个条件的处理是:
- 先查当天0点到第二天0点的时间戳范围,改写SQL:
mysql复制SELECT * FROM operation_log
WHERE operated_at >= '2024-05-20 00:00:00'
AND operated_at < '2024-05-21 00:00:00'
AND operator_id = 88
ORDER BY operated_at DESC
LIMIT 20;
- 用EXPLAIN验证,
type从ALL变成了range,rows从预估800万变成预估几十行。 - 上线后,这条SQL的平均耗时从3秒左右降到75毫秒。
这个案例有两点值得和大家强调:
- 排查的时候不要只盯一个可疑点,先整体看一遍条件结构,再逐层深入。很多慢SQL的问题其实是多因叠加。
- 改写SQL时务必保持业务语义完全一致。
DATE(operated_at) = '2024-05-20'和operated_at >= '2024-05-20 00:00:00' AND operated_at < '2024-05-21 00:00:00'在语义上是等价的,但千万注意时区问题——如果存储的是UTC时间,这个"当天"的边界就得按UTC算,不能按应用服务器的本地时区算。
5.3 覆盖索引:连回表都省掉的终极骚操作
讲到回表成本,就不得不提覆盖索引(Covering Index)这个概念。它和"失效"恰好相反,是用来"救火"的。
覆盖索引指的是:查询需要的所有列,都包含在同一个二级索引里。这样走二级索引扫描时,叶子节点里就有全部需要的数据,不需要再回主键索引取完整行。
最经典的场景是统计数量或判断存在性:
mysql复制SELECT count(*) FROM order_info WHERE status = 1;
如果有联合索引(status, id),优化器会直接遍历这个索引的叶子节点,统计数量,全程不需要访问主键索引。这类count查询在数据量大时性能差异极其明显。
另一个场景是延迟关联(Deferred Join),思路是先通过覆盖索引缩小范围拿到主键,再回表取完整行:
mysql复制SELECT * FROM order_info
JOIN (
SELECT id FROM order_info
WHERE status = 1
ORDER BY create_time DESC
LIMIT 10
) t ON order_info.id = t.id;
内层子查询只查id和create_time,只要能命中覆盖索引,排序和取主键都在索引树内完成,数据量再大也能扛。外层再用这10个id精确回表,回表次数极少。这个技巧在处理大表的"深分页"页面时几乎是必杀技。
覆盖索引的风险在于:索引列越多,写入时的维护成本就越高,索引文件也越大。不是所有查询都适合建覆盖索引,只挑高频、热点的SQL来做。
6. 索引失效的"预防机制":从设计源头降低踩坑概率
6.1 建索引前先问自己:这三个条件都满足了吗
我在做数据库设计评审时,对新加的每个索引都会强制过三个问题:
-
索引左列能接住业务查询的热门前缀吗? 联合索引的列顺序,是以高频WHERE条件里的等值列优先,范围列靠后;如果业务有租户隔离、用户维度的强制条件,那
tenant_id、user_id基本就是索引左列的不二选择。 -
WHERE条件里的每一个等值条件,都能在索引里找到"排它"的位置吗? 如果某些列在SQL里经常被函数包着、被运算改着,要么快速改SQL,要么就要考虑冗余字段——比如预先把日期格式化好的
day_str字段存下来,直接建普通索引,替代对DATE(create_time)的查询。 -
查询结果列是否可以被索引覆盖? 需求里如果高频出现只查三五个固定列的列表页,那一个"覆盖"这几个列的联合索引,往往比纠结"加哪个字段"更能解决问题。
这三个问题在在线业务和高并发接口里尤其重要,因为这类场景里SQL基本固定,索引设计一劳永逸;而临时分析类SQL拆解得再好,也挡不住"分析人员随手写个函数"。
6.2 定期健康检查:Analyze Table与慢查询的日常监控
事务型的MySQL实例,如果业务表经常大批量更新、导入、删除,要学会主动维护统计信息。我一般这样安排日常巡检:
- 每周对写入量大的核心表执行一次
ANALYZE TABLE。 - 每周跑一次慢查询日志分析,专门挑那些
rows预估远大于实际结果集的SQL。 - 开启
performance_schema或sys.schema_unused_indexes,找出完全没有被使用过的冗余索引——索引不是越多越好,每个索引都会拖慢写操作。
另外一个容易被忽略的监控点是索引碎片。频繁的随机删除和更新会让B+树的叶子节点出现大量碎片,逻辑上行的顺序和物理页的顺序错乱,范围扫描的效率会明显下降。对核心大表可以定期执行OPTIMIZE TABLE(注意这个操作会锁表,建议在业务低峰期执行),顺便重建索引。
6.3 写在最后:个人对这些坑的经验总结
索引失效这个问题,本质上是个"认知问题"——你理解B+树的有序性、理解优化器的成本模型,远比背任何失效口诀都管用。我见过很多人把EXPLAIN的结果发到群里问"为什么索引没生效",结果一问字段类型和字符集,答案立刻就出来了。这只能说明基础认知还不够扎实。
如果你平时时间有限,我建议重点把下面这几件事记牢:
- 每次写SQL前,先看一遍索引列有没有被函数、运算或隐式转换包裹。 这是最高频的失效原因,也是最容易通过代码规范避免的。
- 联合索引的列顺序,是索引设计里最重要的决定。 别贪心建大索引,针对真实查询模式设计两到三个联合索引,比一个大而全的索引实用得多。
- 慢SQL排查时,EXPLAIN的type和rows字段比key字段更重要。 possible_keys里列了索引不代表真的使用;rows预估值和实际返回行数的巨大差异,往往意味着统计信息该刷新了。
索引不是银弹,但它依然是关系型数据库里性价比最高的性能手段。理解了失效的条件,你才能真正发挥它的价值,而不是在"失效—加索引—又失效"的循环里耗尽耐心。希望这篇文章里的案例,能帮你在下一次遇到慢查询时,少走几步弯路。
