数据库性能调优中,慢SQL是最常见的瓶颈之一。很多同学面对缓慢的查询,第一反应就是“加索引”。但这种方法常常事倍功半,甚至会因为额外的索引维护成本反而拖累写入性能。
真正高效的优化思路应当是:先理解MySQL优化器的决策逻辑,再进行针对性调整。而理解这一切的基础工具,就是执行计划(Execution Plan)。通过 EXPLAIN 或 EXPLAIN ANALYZE 命令,我们可以清晰地看到MySQL是如何执行一条SQL的——它是否使用了索引、扫描了多少行、是否使用了临时表或文件排序、连接方式是如何选择的。这些信息是诊断慢SQL、制定优化方案的黄金依据。
本文将系统性地讲解MySQL执行计划的解读方法,聚焦核心字段的含义、典型的性能异常信号以及可落地的优化策略,帮助你建立起一套“看计划 -> 找问题 -> 做优化”的系统化思维。
一、生成执行计划:EXPLAIN 与 EXPLAIN ANALYZE
MySQL提供了两种查看执行计划的方式,分别适用于“快速预估”和“精准验证”两种场景,我们需要根据环境合理选择。
1. EXPLAIN:查看优化器预估执行计划
EXPLAIN 是最常用的执行计划命令。它的关键特性是不会真正执行SQL,仅仅基于数据库的统计信息返回优化器预计采用的执行方案。因此执行效率很高,可以在生产环境中放心使用。
使用语法:
EXPLAIN SELECT * FROM order_info
WHERE user_id = 567
AND create_time BETWEEN '2024-01-01' AND '2024-06-01';
返回结果:结果通常包含12列,但聚焦以下9个核心字段,就足以应对绝大多数调优需求:
id, select_type, table, partitions, type, possible_keys, key, rows, Extra
注意事项:
- 结果是基于统计信息估算的。如果表的统计信息过期,预估结果可能与实际执行存在偏差。
partitions 字段在非分区表中恒为 NULL,可以忽略。在MySQL 5.7+版本中该字段为默认列,主要用于指示分区表查询涉及了哪些分区。
2. EXPLAIN ANALYZE:查看真实执行过程
从MySQL 8.0.18版本开始,新增了 EXPLAIN ANALYZE 命令。它会真正执行SQL,并返回运行时收集的性能数据(如真实耗时、实际扫描行数、循环次数等),结果比 EXPLAIN 更为精准。
使用语法:
EXPLAIN ANALYZE SELECT * FROM order_info
WHERE user_id = 567
AND create_time BETWEEN '2024-01-01' AND '2024-06-01';
示例输出:
-> Filter: ((order_info.user_id = 567) and (order_info.create_time between '2024-01-01' and '2024-06-01'))
(cost=1200.00 rows=150) (actual time=0.02..3.80 rows=150 loops=1)
-> Table scan on order_info
(cost=1200.00 rows=120000) (actual time=0.01..2.50 rows=120000 loops=1)
通过 actual time(实际执行耗时)、actual rows(实际扫描行数)等数据,我们可以精准定位性能瓶颈,验证索引是否真的生效、优化方案是否达到了预期。
使用建议:
- 测试环境优先:用于验证优化效果、排查预估结果与实际执行的偏差。
- 生产环境慎用:因为它会真实执行查询。如果查询涉及大表扫描或复杂计算,可能对线上性能造成影响。
- 关注Cost值:输出中的
cost 是优化器估算的执行成本。优化器的核心逻辑就是选择成本最小的执行方案,理解这个值有助于我们明白索引选择的原因。
二、执行计划核心字段解析
1. id:查询块的唯一标识符
id 标识了查询中每个SELECT子句(查询块)的执行优先级,是理解多子查询、多表连接执行顺序的关键。
常见场景与id特征:
- 单表查询:只有一个
id(通常为1)。
- 多表JOIN:所有关联表的
id 相同。
- 普通子查询:子查询的
id 值大于外层查询。
- UNION查询:每个分支有独立的
id,最终结果合并行标记为 UNION RESULT。
执行顺序规则:
id 值越大,执行优先级越高。
id 值相同,按执行计划中从上到下的顺序执行。
UNION RESULT 始终最后执行,用于合并UNION各分支结果。
注意:这个规则主要适用于“非相关子查询”。对于相关子查询(DEPENDENT SUBQUERY),即使它的 id 更大,也不会先执行。它会被外层查询循环驱动执行(外层每返回一行,子查询执行一次)。此时子查询的“执行时机”依赖于外层,不能单纯按id大小排序。这一点容易混淆,需要特别警惕相关子查询带来的高频执行性能风险。
2. select_type:查询类型(标识查询块的性质)
它描述了当前查询块的类型,反映了查询的复杂程度(是否包含子查询、UNION、派生表等)。通过这个字段,可以快速判断是否存在性能高危的查询结构。
常用select_type值及含义(按使用频率排序):
| select_type值 |
含义 |
性能注意点 |
| SIMPLE |
简单查询,无任何子查询、UNION、派生表 |
性能最优,没有额外开销 |
| PRIMARY |
复杂查询中的最外层查询块 |
没有直接性能开销,是其他查询块的外层容器 |
| SUBQUERY |
SELECT/WHERE子句中的普通子查询 |
单次执行,性能风险较低 |
| DEPENDENT SUBQUERY |
相关子查询,依赖外层查询的变量,循环执行 |
⚠️ 高危:外层返回N行则执行N次,容易引发性能问题 |
| DERIVED |
FROM子句中的子查询,会生成派生表 |
⚠️ 中高危:需要生成临时表存储中间结果,有IO和内存开销 |
| UNION |
UNION查询中第二个及以后的SELECT分支 |
没有直接开销,需要注意UNION的去重成本 |
| UNION RESULT |
UNION查询的结果合并行 |
需要临时表存储中间结果,有去重(DISTINCT)开销 |
性能预警信号:当执行计划中出现 DEPENDENT SUBQUERY、DERIVED、UNION RESULT 时,需要格外警惕!这些类型往往伴随着临时表的创建、高频循环执行、数据重复处理等额外开销,是慢SQL的常见诱因。
补充:MySQL 8.0优化器支持派生表合并(Derived Table Merging),大多数简单的 DERIVED 类型子查询会被自动合并为 SIMPLE 查询,从而减少临时表开销。如果在8.0版本中仍然看到 DERIVED,通常说明子查询逻辑较为复杂(例如包含了GROUP BY、DISTINCT、LIMIT等),导致优化器无法合并,这时就需要我们重点分析。
3. table:当前操作的表名/查询块标识
显示执行计划当前行对应的操作对象,即MySQL正在处理的表或查询块。
常见显示形式:
- 物理表:直接显示表名(如:
order_info, user)。
- 派生表:显示为
derivedN(N为该派生表对应的 id 值,如:derived2)。
- UNION结果:显示为
unionM,N(M、N为UNION各分支的 id 值,如:union1,2)。
- 临时表:部分场景下显示为
tmp_table。
解读技巧:将 id、select_type 和 table 结合起来看,可以快速定位“某张表/某个查询块”在整个查询中的位置和作用。例如,table=derived3 + id=3 + select_type=DERIVED,可以直接判断这是“id为3的派生表查询块”。
4. type:访问类型(性能核心字段,没有之一)
type 表示MySQL查找表中数据行的具体方式,它直接反映了数据查找的效率,是判断是否合理使用索引、排查全表扫描的最关键指标。
性能排序:以下是常见的访问类型,性能从高到低排列。我们的优化核心目标就是让 type 尽可能地向高等级靠拢:system > const > eq_ref > ref > range > index > ALL。
各访问类型详细解读:
| type值 |
中文名称 |
核心适用场景 |
性能等级 |
关键判断标准 |
| system |
系统表访问 |
表为系统表且仅包含一行数据(如:mysql.user表的部分场景) |
最优(极罕见) |
特殊场景,实际业务中几乎遇不到 |
| const |
常量查询 |
主键/唯一索引等值查询,且仅返回一行结果 |
最优 |
type=const + key为主键/唯一索引 |
| eq_ref |
等值引用 |
多表JOIN时,使用主键/唯一索引作为连接条件 |
优秀 |
多表连接场景的理想访问类型 |
| ref |
非唯一索引等值查询 |
普通非唯一索引等值查询(如:WHERE user_id=567,user_id为普通索引) |
良好 |
type=ref + key为非唯一索引 |
| range |
索引范围扫描 |
索引上的范围查询(BETWEEN/IN/>/</>=/<=等) |
中等 |
仅扫描索引的指定范围,不遍历全索引 |
| index |
全索引扫描 |
遍历整个索引树(未走全表扫描,但扫描了全部索引节点) |
较差 |
索引体积远小于表体积时,比ALL略优 |
| ALL |
全表扫描 |
遍历整个数据表,未使用任何索引 |
最差(高危) |
大表中出现此类型,几乎必然是慢SQL |
优化目标:在业务查询中,我们应尽量让 type 达到 range 及以上。如果在大表查询中看到 type=ALL,基本可以判定为“未走索引”或“索引失效”,这是首要的优化点。
ALL类型常见诱因:
- 查询字段未建立任何索引。
- 索引失效(例如:对索引字段使用函数、LIKE以通配符开头
%abc、OR连接无索引字段)。
- 优化器认为全表扫描更快(这通常仅适用于超小表,数据量小于100行)。
补充说明:MySQL 5.7及以下版本仅支持Nested Loop(嵌套循环)和Merge Join(归并连接)算法。Hash Join从MySQL 8.0.18才开始正式支持。因此,在低版本中看到的连接行为不会是Hash Join,避免误判。
5. possible_keys:可能使用的候选索引
显示MySQL优化器根据查询条件,认为可能适用的索引列表。这是我们判断“是否有可用索引”的第一依据。
要点解读:
- 该字段仅仅是候选索引列表,优化器不一定最终会使用(需要结合
key 字段判断)。
- 如果值为
NULL,说明查询条件中没有任何可匹配的索引字段,MySQL只能选择全表扫描。
- 候选索引的数量由查询条件中的索引字段组合决定,并非越多越好。
示例:查询 WHERE user_id=567 AND create_time>'2024-01-01',如果表中有单索引 idx_user_id、idx_create_time 和联合索引 idx_user_create,那么 possible_keys 会包含这三个索引。
6. key:实际使用的索引
显示MySQL优化器最终选择用于执行查询的索引。这是判断“索引是否真正生效”的核心指标,需要与 possible_keys 对比分析。
要点解读:
- 如果值为
NULL,表示优化器未使用任何索引(无论 possible_keys 是否有值)。
key 的值一定是 possible_keys 中的一个(或为空),不会出现候选列表之外的索引。
- 即使
possible_keys 有值,key 仍可能为 NULL,常见原因有:
- 索引字段的区分度极低(例如:“性别”字段仅含男/女两个值),优化器认为全表扫描反而更快。
- 数据库统计信息过期,优化器无法准确判断索引的效率。
- 查询的数据量过大(例如:超过表数据的30%),索引回表的成本高于全表扫描。
解读技巧:possible_keys 有值但是 key=NULL,是典型的“索引可用但未使用”场景,需要针对性排查(例如:更新统计信息、优化查询条件、调整索引设计)。
7. key_len:实际使用的索引字节长度
表示MySQL在执行查询时,实际利用到的索引列的字节总长度。它是判断联合索引是否被充分利用、是否遵循最左前缀原则的核心依据,也是排查“隐式类型转换导致索引失效”的重要线索。
联合索引的生效遵循“最左前缀原则”。通过 key_len 的数值,我们可以精准判断:优化器使用了联合索引的前几列、是否有列未被利用。例如,联合索引 (a, b, c),如果 key_len 仅为a列的字节长度,说明只有a列生效,b、c列未被利用。
常用字段的key_len计算规则(基于MySQL默认字符集utf8mb4,为近似值,便于快速判断):
| 字段类型 |
字节长度(非NULL) |
字节长度(允许NULL) |
备注 |
| TINYINT |
1 |
1+1=2 |
额外1字节为NULL标志位 |
| INT |
4 |
4+1=5 |
常规整数类型 |
| BIGINT |
8 |
8+1=9 |
大整数类型 |
| CHAR(N) |
N×4 |
N×4+1 |
固定长度字符串,按最大长度计算 |
| VARCHAR(N) |
如果最大字节数 ≤ 255,则字节长度 = 最大字节数 + 1(长度前缀1字节);如果最大字节数 > 255,则字节长度 = 最大字节数 + 2(长度前缀2字节);允许 NULL 时再额外 +1 字节。 |
变长字符串,按最大可能字节长度计算 |
|
重要计算原则:
- 允许NULL的字段,会额外占用1字节的NULL标志位;NOT NULL字段无此开销。
- 字符串类型(VARCHAR/CHAR)的字节长度与字符集强相关:utf8为×3,utf8mb4为×4(需要存储emoji等4字节字符),gbk为×2。
key_len 是优化器预估的最大可能长度,并非实际数据的字节长度,用于统一判断索引利用情况。
解读技巧:
- 计算联合索引的理论总长度,与实际的
key_len 对比,差值即为未被利用的索引列长度。
- 如果
key_len 比理论长度小,且排除了“最左前缀原则”,那么大概率是隐式类型转换导致(如字符串字段存数字,查询时未加引号),使得索引仅部分生效。
8. rows:预估扫描行数
表示MySQL优化器预估为得到查询结果,需要扫描的行数。它是判断查询IO开销大小的重要指标,数值越小,查询效率越高。
要点解读:
- 该值为估算值,基于数据库统计信息计算,并非实际扫描行数(实际行数需通过
EXPLAIN ANALYZE 的 actual rows 查看)。
- 数值越大,说明MySQL需要扫描的数据越多,IO开销越大,查询越慢。
- 多表连接场景的特殊解读:
rows 表示当前表相对于前一个表查询结果的单次循环扫描行数,而不是整个查询的总扫描行数。例如,外层表 rows=100,内层关联表 rows=50,则内层实际预估扫描行数为 100×50=5000 行。
优化目标:通过索引优化、查询条件优化,尽可能降低 rows 值,减少数据扫描范围。
9. filtered:行过滤比例(MySQL 5.7+默认展示)
表示从存储引擎返回的行中,经过MySQL Server层WHERE条件过滤后,保留的行占比(百分比)。取值范围0~100,数值越高,过滤效率越好。
要点解读:
filtered=100:表示存储引擎返回的行全部满足条件,无额外过滤开销,过滤效率最优。
filtered 数值极低(例如:<10%):表示存储引擎返回了大量无效数据,需要在Server层过滤后丢弃。这是典型的索引过滤效果差的表现,常见原因有:
- 索引区分度低,无法精准过滤数据。
- 查询条件未包含在索引中,只能回表后过滤。
- 索引仅使用了部分列,过滤条件不充分。
性能预警:如果 rows 较大(例如:>10000)且 filtered 极低(例如:<5%),说明查询“扫描了大量数据,最终仅保留少量有效数据”,IO和CPU开销极大,这是慢SQL的核心特征之一。
显示MySQL执行查询过程中的额外细节信息。这些信息无法通过其他字段体现,却直接反映了查询的性能特征(是否使用覆盖索引、是否触发文件排序/临时表、是否开启索引下推等),是排查性能问题的黄金字段。
常用Extra值及性能解读(按性能风险等级排序):
| Extra值 |
核心含义 |
性能等级 |
优化方向 |
| Using index |
覆盖索引扫描,无需回表 |
最优 |
保留该特性,尽可能将查询字段纳入索引 |
| Using index condition |
索引下推(ICP),引擎层提前过滤 |
优秀 |
确保MySQL 5.6+开启ICP(默认开启) |
| Using where |
Server层过滤,无额外开销 |
一般 |
如可优化,将过滤条件纳入索引,实现引擎层过滤 |
| Using join buffer (Block Nested Loop) |
多表连接未走索引,使用连接缓冲区 |
中危 |
为连接条件添加索引,消除连接缓冲区 |
| Using filesort |
无法利用索引排序,需要额外文件排序 |
高危 |
创建“过滤+排序”联合索引,利用索引排序 |
| Using temporary |
需要创建临时表存储中间结果 |
高危 |
为GROUP BY/DISTINCT字段添加索引,避免临时表 |
| Using filesort + Using temporary |
同时触发文件排序和临时表 |
极危 |
紧急优化,几乎必然是慢SQL |
3个最常用的关键Extra值详解:
① Using index(覆盖索引):查询的所有字段(SELECT列、WHERE条件列、ORDER BY列、GROUP BY列)均包含在某一个索引中,MySQL无需回表查询物理表,仅通过遍历索引即可得到所有结果。这种索引称为覆盖索引,是SQL优化的终极目标之一。
- 核心价值:避免了“索引扫描 + 回表查询”的两次IO操作,能大幅提升查询效率,尤其适用于大表的高频查询。
- 实现条件:并非仅SELECT字段在索引中即可,需要满足所有涉及的字段(SELECT、WHERE、ORDER BY、GROUP BY)均在同一索引中。例如,查询
SELECT id, name FROM user WHERE age>30 ORDER BY name,需要创建联合索引 (age, name, id)(或 (age, name),因为主键id默认包含在二级索引中)才能实现 Using index。
② Using index condition(ICP索引下推,MySQL 5.6+):开启索引下推后,存储引擎(InnoDB)在遍历索引时,可利用索引中的非前缀列提前过滤不满足条件的数据,仅将符合条件的数据行回表查询,从而大幅减少回表次数。
- 核心价值:解决了“最左前缀原则下,非前缀列无法过滤”的问题,提升了range扫描的效率。例如,联合索引
(user_id, create_time),查询 WHERE user_id>100 AND create_time>'2024-01-01',ICP可在引擎层利用 create_time 进行过滤,减少回表行数。
- 注意:ICP仅适用于range/ref/eq_ref等索引扫描类型,全表扫描(ALL)无此特性。该特性默认开启(参数
optimizer_switch='index_condition_pushdown=on')。
③ Using filesort / Using temporary:这两个值是慢SQL的最常见诱因,通常伴随出现。核心原因是排序/分组字段未包含在索引中,MySQL需要额外处理:
- Using filesort:MySQL无法利用索引的有序性进行排序,需要将数据加载到内存(或磁盘)中进行额外排序。排序效率随数据量呈指数级下降。
- Using temporary:MySQL需要创建临时表(内存临时表或磁盘临时表)来存储GROUP BY/DISTINCT/UNION的中间结果,有额外的IO和内存开销。如果临时表过大,会从内存转为磁盘,性能急剧下降。
11. ref:索引匹配的参照依据
显示MySQL在使用索引查找数据时,索引列与什么类型的值进行匹配。它是判断索引是否被高效、正确使用的关键字段,也是排查“索引匹配异常”的重要线索。
常用ref值及含义:
| ref值 |
核心含义 |
索引使用效果 |
| const |
与常量值匹配(如:WHERE id=100) |
最优,索引利用效率最高 |
| func |
与函数计算结果匹配(如:WHERE name=CONCAT('a', 'b')) |
一般,函数可能导致索引部分失效 |
| db.table.column |
多表连接时,与另一张表的列匹配(如:JOIN u.id = o.user_id) |
良好,正常的联表索引匹配 |
| NULL |
注意:当 type = ref 时,ref 字段一定不为NULL。如果看到 ref = NULL,通常出现在 type = range 或 index 场景,此时表示MySQL未使用“等值匹配”方式访问索引,需要结合具体查询判断是否因隐式转换或索引设计不当导致效率低下。 |
|
要点解读:
ref=const 是索引的最优使用方式,适用于主键/唯一索引等值查询。
- 多表连接时,
ref 显示关联表的列名,说明连接条件使用了索引,匹配方式合理。
三、执行计划中的6大典型性能异常与落地优化策略
通过解读执行计划的核心字段,我们可以快速识别出MySQL执行过程中的性能异常信号,并采取针对性的优化措施,这往往是数据库优化工作中最核心的部分。
异常1:type = ALL(全表扫描),最常见的慢SQL诱因
异常表现:type=ALL、key=NULL、rows 数值极大(大表中通常>10000),Extra字段通常为 Using where。
异常原因:
- 查询条件中的字段未建立任何索引。
- 索引失效(对索引字段使用函数、LIKE左模糊
%abc、OR连接无索引字段、隐式类型转换)。
- 优化器认为全表扫描更快(仅适用于超小表,大表出现此情况均为异常)。
- 统计信息过期,优化器无法准确判断索引效率。
优化策略:
- 基础优化:为查询条件中的核心字段(过滤性强的字段)建立单索引或联合索引。
- 避免索引失效写法:
- 函数操作改为范围查询(如:
WHERE YEAR(create_time)=2024 → WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31')。
- LIKE左模糊改为右模糊(如:
LIKE '%abc' → LIKE 'abc%';若必须左模糊,可考虑使用全文索引)。
- OR连接改为UNION(如:
WHERE a=1 OR b=2 → SELECT * FROM t WHERE a=1 UNION SELECT * FROM t WHERE b=2,前提是a、b字段分别有索引)。
- 消除隐式类型转换:保证查询条件的数值类型与字段类型一致(如字符串字段查询时加引号:
WHERE name='123' 而不是 WHERE name=123)。
- 更新统计信息:执行
ANALYZE TABLE table_name;,让优化器获取准确的表数据分布。
异常2:possible_keys有值,但是key = NULL(索引可用但未使用)
异常表现:possible_keys 包含1个或多个索引,但 key=NULL,type 通常为ALL或index。这说明优化器有候选索引,但主动放弃了使用。
异常原因:
- 索引字段区分度极低(如:“状态”字段仅含0/1两个值),优化器认为“索引扫描+回表”的成本高于全表扫描。
- 数据库统计信息过期,优化器无法准确评估索引的过滤效果。
- 查询数据量过大(如:超过表数据的30%),索引的优势无法体现。
- 联合索引未遵循最左前缀原则,仅使用了非前缀列。
优化策略:
- 更新统计信息:优先执行
ANALYZE TABLE table_name;,这是最常见、最易落地的解决方案。
- 优化索引设计:
- 对于低区分度字段,与高区分度字段组成联合索引(如:
(user_id, status),其中user_id为高区分度字段),以提升整体过滤效果。
- 删除低效的单索引,避免优化器选择错误的索引。
- 优化查询条件:增加高区分度的过滤条件,减少查询数据量(如:在
WHERE status=1 基础上,增加 create_time>'2024-01-01')。
- 临时验证:可谨慎使用
FORCE INDEX 强制使用索引来测试效果(仅作验证,不建议生产环境长期使用),例如:
SELECT * FROM order_info FORCE INDEX (idx_user_id) WHERE user_id=567;
异常表现:Extra 包含 Using filesort,通常伴随 ORDER BY 子句。说明MySQL无法利用索引排序,需要额外执行排序操作。
异常原因:ORDER BY 的排序字段没有包含在过滤条件的索引中,或排序字段与索引字段的顺序不一致,导致无法利用索引的有序性。
优化策略:创建“过滤条件 + 排序字段”的联合索引,让MySQL利用索引的有序性直接返回排好序的数据,从而消除文件排序。这是唯一有效的落地方案。
实战示例:
- 原查询:
SELECT * FROM user WHERE age>30 ORDER BY name; (假设age有单索引,name无索引)
- 问题:age索引仅用于过滤,无法用于排序name,因此触发Using filesort。
- 优化方法:创建联合索引
idx_age_name (age, name),实现“过滤+排序”一体化,消除文件排序。
注意事项:
- 联合索引的顺序需遵循“过滤条件在前,排序字段在后”。
- 排序的方向(ASC/DESC)需要与索引的顺序一致(MySQL 8.0+支持降序索引,可解决混合排序问题)。
- 如果查询包含多个排序字段,需要将所有排序字段按顺序纳入联合索引(如:
ORDER BY name, create_time,则索引应为 (age, name, create_time))。
异常表现:Extra 包含 Using temporary,通常伴随 GROUP BY 或 DISTINCT 子句。说明MySQL需要创建临时表来存储中间结果。
异常原因:GROUP BY/DISTINCT 的分组/去重字段未建立索引,优化器无法利用索引的有序性进行分组,只能通过临时表存储中间结果。
优化策略:为 GROUP BY/DISTINCT 的字段建立索引(单索引或联合索引),利用索引的有序性直接分组,避免临时表的创建。
实战示例:
- 原查询:
SELECT age, COUNT(*) FROM user GROUP BY age; (假设age无索引)
- 问题:无索引,触发Using temporary。
- 优化方法:为age建立单索引
idx_age,或创建包含查询字段的覆盖索引 idx_age_count (age, id),以消除临时表。
延伸优化:
DISTINCT 在MySQL中会被优化器重写为 GROUP BY,因此优化策略与 GROUP BY 一致。
- 如果GROUP BY同时伴随
ORDER BY,需要将排序字段也纳入索引,避免同时触发 Using temporary 和 Using filesort。
- 对于多字段分组,需创建联合索引(分组字段按顺序排列)。
异常5:key_len 小于联合索引总长度(联合索引未充分利用)
异常表现:使用联合索引时,key_len 的数值仅为联合索引前N列的字节长度,小于索引总长度。这说明联合索引的后续列未被利用,索引生效不充分。
异常原因:违反最左前缀原则,这是联合索引未充分利用的唯一核心原因。常见场景:
- 跳过联合索引的前缀列,直接使用后续列作为查询条件(如:索引
(a, b, c),查询 WHERE b=1 AND c=2)。
- 前缀列使用了函数、隐式类型转换等操作,导致前缀列索引失效,后续列无法生效。
- 前缀列为范围查询(如:>、<、BETWEEN),导致后续列无法利用索引(MySQL 5.7及以下版本)。
优化策略:
- 遵循最左前缀原则:修改查询条件,加入联合索引的前缀列(如:索引
(a, b, c),查询时增加a的等值条件 WHERE a=1 AND b=1 AND c=2)。
- 优化前缀列的查询方式:避免对前缀列使用函数、隐式类型转换,保证前缀列索引正常生效。
- 重新设计索引:如果确实无法加入前缀列的查询条件,可为后续列单独创建新的联合索引(如:
(b, c)),但需要权衡索引维护成本(索引越多,写入性能越低)。
- 版本升级/优化:MySQL 8.0+支持ICP增强,可在范围查询后利用后续列过滤,但合理的索引设计仍是基础。
异常6:type = ref但是rows很大(索引区分度低)
异常表现:type=ref(看似走了高效的索引扫描),但是 rows 数值极大(例如>10000)。这说明索引虽然生效了,但是过滤效果极差,仍然需要扫描大量数据。
异常原因:索引字段的区分度极低(重复值过多)。例如,“订单状态”字段(仅含0-未支付、1-已支付、2-已取消)、“性别”字段等。即使为这些字段建立了索引,也无法精准过滤数据。
优化策略:
- 创建联合索引:将低区分度字段与高区分度字段组合成联合索引,提升整体过滤效果。
- 示例:原索引为
idx_status(订单状态,区分度低),查询为 WHERE status=1 AND user_id=567。
- 优化方法:创建联合索引
idx_user_status (user_id, status)(user_id为高区分度字段),可大幅降低 rows 值。
- 增加过滤条件:在查询中加入高区分度的过滤条件(如:时间、用户ID等),减少扫描行数。
- 评估索引的必要性:如果低区分度字段的查询频率极低,可以考虑删除该单索引,避免索引维护成本拖慢写入性能(记住,索引并非越多越好)。
掌握这些执行计划的核心解读方法和异常处理策略,你就能从“盲目加索引”转向“精准分析、有效优化”的新阶段。如果你想深入研究更多关于后端架构或数据库优化的知识,可以访问我们的数据库/中间件/技术栈板块。希望这篇文章能帮助大家在日常工作中更高效地进行MySQL性能调优。如果你在实践中遇到了其他复杂场景,也欢迎到云栈社区与大家一起交流探讨。