在 Java 开发中,我们常常会遇到数据库性能瓶颈的问题。一条看似简单的 SQL 语句,在数据量增长到一定规模后,可能会从毫秒级响应变成秒级甚至分钟级响应,直接拖慢整个应用的性能。此时,你是否曾困惑于:为什么这条 SQL 突然变慢了?索引明明建了,为什么没生效?到底是哪里出了问题?
答案就藏在 MySQL 的EXPLAIN命令里。EXPLAIN就像一面 “照妖镜”,能帮你看透 SQL 语句的执行计划,揭示 MySQL 是如何处理你的 SQL 的 —— 它会告诉你表的访问顺序、数据读取操作的类型、索引的使用情况、扫描的行数等等关键信息。掌握EXPLAIN的使用方法,是每个 Java 资深技术专家必备的技能,也是优化 SQL 性能的核心武器。
本文将带你全面解锁EXPLAIN的奥秘,从基础概念到实战技巧,从字段解析到案例分析,让你彻底搞懂EXPLAIN,轻松写出高性能的 SQL 语句。
EXPLAIN是 MySQL 提供的一个诊断工具,它可以模拟 MySQL 优化器执行 SQL 语句的过程,输出 SQL 语句的执行计划。通过分析执行计划,我们可以了解 SQL 的执行细节,从而发现潜在的性能问题,进行针对性优化。
使用EXPLAIN非常简单,只需在 SQL 语句前加上EXPLAIN关键字即可。例如:
EXPLAIN SELECT * FROM user WHERE id = 1;
对于UPDATE、DELETE、INSERT语句,也可以使用EXPLAIN,但需要注意的是,EXPLAIN不会实际执行这些语句,只会分析其执行计划。例如:
EXPLAIN UPDATE user SET name = '张三' WHERE id = 1;
此外,EXPLAIN FORMAT=JSON可以输出 JSON 格式的执行计划,包含更详细的信息:
EXPLAIN FORMAT=JSON SELECT * FROM user WHERE id = 1;
EXPLAIN的输出通常包含 12 个字段:id、select_type、table、type、possible_keys、key、key_len、ref、rows、filtered、Extra。下面我们逐个解析每个字段的含义和作用。
id字段表示查询中每个 select 子句的序列号,或者说是查询的执行顺序标识。它有以下几种情况:
EXPLAIN SELECT u.name, o.order_no FROM user u JOIN order o ON u.id = o.user_id;
输出中u和o的id相同,说明 MySQL 可能先访问user表,再访问order表(具体顺序可能受优化器影响)。
EXPLAIN SELECT * FROM user WHERE id = (SELECT user_id FROM order WHERE order_no = '2023001');
子查询的id会大于主查询的id,说明 MySQL 会先执行子查询(SELECT user_id FROM order ...),再执行主查询。
EXPLAIN SELECT * FROM (SELECT id FROM user WHERE age > 18) t;
外层查询的id为 1,内层子查询的结果会被放入临时表t,临时表的id为 NULL。
select_type字段表示查询的类型,用于区分普通查询、联合查询、子查询等复杂查询。常见的取值有:
table字段表示当前行正在访问的表的名称。如果查询中使用了别名,这里会显示别名。如果是派生表或临时表,可能会显示类似derived2(数字表示对应的子查询 id)的名称。
type字段表示 MySQL 访问表中数据的方式,又称 “访问类型”。它是判断查询性能的重要指标,从好到坏的顺序如下:
system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
下面详细介绍几种常见的类型:
优化建议:在实际开发中,应保证查询的type至少达到range级别,最好能达到ref或const级别。若出现ALL,通常需要添加或优化索引。
possible_keys字段表示 MySQL 在查询时可能会使用的索引列表。这是 MySQL 根据查询条件估算的可能用到的索引,实际不一定会使用。
key字段表示 MySQL 在查询时实际使用的索引。如果key为 NULL,说明没有使用索引。
key字段是判断索引是否生效的核心依据。若possible_keys不为空但key为空,可能是因为 MySQL 优化器认为全表扫描比使用索引更快(如表中数据量极少时),或索引失效(如使用了函数操作索引字段)。
key_len字段表示 MySQL 在查询时使用的索引的长度(以字节为单位)。它可以帮助我们判断索引的使用情况,尤其是组合索引的使用情况。
key_len的计算规则与字段类型、字符集、是否为 NULL 有关:
通过key_len可以判断组合索引是否被充分利用:key_len越大,说明使用的索引部分越多。
ref字段表示在使用索引查询时,哪些列或常量被用来与索引进行比较。
rows字段表示 MySQL 预估需要扫描的行数,用于评估查询的代价。rows值越小,查询效率越高。
注意:rows是一个预估值,不是实际扫描的行数,但通常能反映查询的大致效率。优化时,应尽量减少rows的值。
filtered字段表示符合查询条件的记录占扫描行数的百分比(范围 0-100)。filtered值越大,说明过滤效果越好,无用数据扫描越少。
rows × filtered / 100 可以估算出最终返回的记录数。
Extra字段包含了 MySQL 执行查询时的额外信息,这些信息对于分析查询性能非常重要。常见的取值有:
理论结合实践才能真正掌握EXPLAIN。下面通过多个实战案例,展示如何使用EXPLAIN分析并优化 SQL 语句。
场景:查询年龄为 18 的用户信息,表 user 有 100 万行数据,age 字段无索引。
SQL 语句:
SELECT * FROM user WHERE age = 18;
Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
user |
ALL |
NULL |
NULL |
NULL |
NULL |
1000000 |
10.00 |
Using where |
分析:type=ALL(全表扫描),key=NULL(未使用索引),rows=1000000(扫描 100 万行),性能极差。
优化方案:为 age 字段添加索引。
ALTER TABLE user ADD INDEX idx_age (age);
优化后 Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
user |
ref |
idx_age |
idx_age |
4 |
const |
10000 |
100.00 |
效果:type=ref(使用索引),rows=10000(扫描行数大幅减少),性能显著提升。
场景:查询用户信息并按 name 排序,name 字段无索引。
SQL 语句:
SELECT * FROM user WHERE age = 18 ORDER BY name;
Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
user |
ref |
idx_age |
idx_age |
4 |
const |
10000 |
100.00 |
Using where; Using filesort |
分析:Extra为Using filesort,表示需要额外的排序操作,性能较差。
优化方案:创建组合索引idx_age_name(age, name),既满足查询条件,又支持排序。
ALTER TABLE user DROP INDEX idx_age;
ALTER TABLE user ADD INDEX idx_age_name (age, name);
优化后 Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
user |
ref |
idx_age_name |
idx_age_name |
4 |
const |
10000 |
100.00 |
Using index condition |
效果:Extra中Using filesort消失,排序操作通过索引完成,性能提升。
场景:查询用户姓名并去重,name 字段无索引。
SQL 语句:
SELECT DISTINCT name FROM user;
Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
user |
ALL |
NULL |
NULL |
NULL |
NULL |
1000000 |
100.00 |
Using temporary |
分析:Extra为Using temporary,表示需要创建临时表存储去重结果,性能较差。
优化方案:为 name 字段添加索引,索引天然有序且不重复,可避免临时表。
ALTER TABLE user ADD INDEX idx_name (name);
优化后 Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
user |
index |
NULL |
idx_name |
42 |
NULL |
1000000 |
100.00 |
Using index |
效果:Extra为Using index(覆盖索引),Using temporary消失,性能提升。
场景:查询有订单的用户信息,使用子查询。
SQL 语句:
SELECT * FROM user WHERE id IN (SELECT user_id FROM order);
Explain 输出(可能的结果):
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
PRIMARY |
user |
ALL |
PRIMARY |
NULL |
NULL |
NULL |
100000 |
100.00 |
Using where |
2 |
SUBQUERY |
order |
index |
idx_user_id |
idx_user_id |
4 |
NULL |
500000 |
100.00 |
Using index |
分析:主查询type=ALL(全表扫描),性能较差。MySQL 优化器对某些子查询的优化不够理想,可能导致全表扫描。
优化方案:将子查询改为JOIN查询,通常性能更好。
SELECT DISTINCT u.* FROM user u JOIN order o ON u.id = o.user_id;
优化后 Explain 输出:
id |
select_type |
table |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
Extra |
1 |
SIMPLE |
u |
index |
PRIMARY |
PRIMARY |
4 |
NULL |
100000 |
100.00 |
Using index |
1 |
SIMPLE |
o |
ref |
idx_user_id |
idx_user_id |
4 |
test.u.id |
5 |
100.00 |
Using index |
效果:主查询type=index,通过JOIN利用索引,扫描行数减少,性能提升。
EXPLAIN不仅适用于 SELECT 语句,也适用于 UPDATE 和 DELETE 语句。分析这些语句的执行计划,可以避免因更新 / 删除操作导致的全表扫描,影响数据库性能。
EXPLAIN DELETE FROM user WHERE age = 18;
若输出type=ALL,说明会全表扫描删除,应添加 age 索引;若type=ref,则使用了索引,性能更优。
联合索引遵循最左前缀原则:即索引的生效顺序从左到右,若查询条件不包含最左列,则索引不生效。通过EXPLAIN可以验证这一点。
通过EXPLAIN的key字段,可以识别索引失效的场景,常见的有:
优化方法:避免在索引字段上使用函数或运算,尽量使用等于(=)、IN等操作。
EXPLAIN FORMAT=JSON输出的 JSON 格式执行计划包含更详细的信息,如成本估算(cost_info)、数据读取方式(access_type)等,适合深入分析复杂查询。
EXPLAIN FORMAT=JSON SELECT * FROM user WHERE id = 1;
输出结果中包含"cost_info": {"query_cost": "1.00"}(查询成本)、"access_type": "const"等信息。
MySQL 的EXPLAIN命令是 SQL 优化的 “瑞士军刀”,它能帮你看透 SQL 的执行本质,发现潜在的性能问题。通过本文的学习,你应该已经掌握了EXPLAIN的各个字段的含义,以及如何利用它来分析和优化 SQL 语句。
核心要点回顾:
在实际开发中,养成写 SQL 前先使用EXPLAIN分析的习惯,能帮你提前规避性能问题。记住,好的 SQL 不是写出来的,而是优化出来的,而EXPLAIN就是你优化之路上的最佳伙伴。
希望本文能让你对EXPLAIN有更深入的理解,让你的 SQL 性能更上一层楼!如果你有更多关于EXPLAIN的使用技巧或实战案例,欢迎在评论区分享交流。