MYSQL中优化器解释Explain的各字段说明和实际使用示例
MySQL EXPLAIN工具详解 EXPLAIN是MySQL性能分析和SQL优化的核心工具,通过模拟优化器执行SQL语句,输出执行计划关键信息。主要字段包括: id - 查询标识符,显示执行顺序 select_type - 查询类型(如SIMPLE/PRIMARY/SUBQUERY) type - 连接类型(性能关键指标,从最优system到最差ALL) key - 实际使用索引 rows -
EXPLAIN是MYSQL必不可少的一个分析工具,主要用来测试sql语句的性能及对sql语句的优化,或者说模拟优化器执行SQL语句。
官方文档地址:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html
一、Explain输出各字段详解
基本用法:
EXPLAIN SELECT * FROM users WHERE age > 25;
-- 或者
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE age > 25; -- JSON格式
以下输出的各列的表字段详解表:
| 字段 | JSON Name | 说明 | 优化意义 |
|---|---|---|---|
| id | select_id | 查询序列号,相同id按顺序执行,不同id值越大优先级越高 | 定位复杂查询执行顺序,识别子查询层级 |
| select_type | None | 查询类型 | 识别简单查询或复杂子查询结构 |
| table | table_name | 访问的表名 | 确定查询涉及的表对象,含别名和<unionM,N>等特殊标记 |
| partitions | partitions | 匹配的分区 | 分区表查询时显示命中的分区名 |
| type(关键) | access_type | 访问类型,性能排序:system > const > eq_ref > ref > range > index > ALL | SQL优化的核心指标,决定数据检索效率 |
| possible_keys | possible_keys | 可能使用的索引 | 检查索引设计是否合理 |
| key(关键) | key | 实际使用的索引 | 验证优化器最终选择的索引 |
| key_len(关键) | key_length | 索引使用的字节数 | 计算复合索引中使用的字段长度,验证索引利用率 |
| ref | ref | 索引关联的列或常量 | 显示与索引比较的列或常量,检查关联条件 |
| rows(关键) | rows | 预估扫描行数 | 数值越小性能越好,大数据量优化 |
| filtered | filtered | 存储引擎过滤后的剩余比例 | 查询效率核心指标,100%表示完全过滤 |
| Extra(关键) | None | 额外执行信息 | 揭示潜在性能问题(如临时表/文件排序) |
现在来挨个介绍各列含义:
(一) id - 查询标识符
SELECT标识符。这是查询中SELECT的顺序编号。如果该行引用的是其他行的联合结果,则该值可为NULL。此时,表列会显示类似<unionM,N>的值,以表明该行引用的是id值为M和N的行的联合结果。
规则:
- id相同:执行顺序从上到下
- id不同:id值越大,优先级越高,越先执行
- id为NULL:表示是其他查询的联合结果
(二) select_type - 查询类型
SELECT类型可为下表所示任一种。JSON格式的EXPLAIN会将SELECT类型作为查询块的属性显示,除非其为SIMPLE或PRIMARY类型。表中还列出了JSON名称(如适用)。
| select_type | 含义 | 示例 | 优化意义 |
|---|---|---|---|
| SIMPLE | 简单查询,不含子查询或UNION | SELECT * FROM users |
通常无需优化,但需确保WHERE条件有效使用索引 |
| PRIMARY | 主查询(最外层查询) | 包含子查询时最外层的SELECT | 主查询结果集大小影响后续操作,需合理筛选数据 |
| UNION | UNION中的第二个及后续SELECT | SELECT * FROM t1 UNION SELECT * FROM t2 |
每个UNION查询应独立优化,确保使用索引 |
| DEPENDENT UNION | 第二个或更后面的SELECT语句在UNION中,依赖于外部查询 | SELECT * FROM t1 WHERE a IN (SELECT a FROM t2 UNION SELECT a FROM t3) |
依赖外部查询会导致重复执行,性能较差,考虑重写查询 |
| UNION RESULT | UNION的结果 | SELECT * FROM t1 UNION SELECT * FROM t2的结果集 |
关注UNION结果的去重和排序操作,可能导致临时表 |
| SUBQUERY | 子查询中的第一个SELECT | SELECT * FROM t1 WHERE id = (SELECT id FROM t2) |
子查询应能有效使用索引,避免全表扫描 |
| DEPENDENT SUBQUERY | 子查询中的第一个SELECT,依赖于外部查询 | SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id = t1.id) |
依赖外部查询会导致对外部查询每一行都执行子查询,性能差 |
| DERIVED | 派生表(FROM子句中的子查询) | SELECT * FROM (SELECT * FROM t1) AS t |
派生表会产生临时表,可能影响性能,考虑使用JOIN重写 |
| MATERIALIZED | 物化子查询 | SELECT * FROM t1 WHERE id IN (SELECT id FROM t2) |
MySQL会将子查询结果物化为临时表,注意临时表大小和索引使用 |
| UNCACHEABLE SUBQUERY | 无法缓存结果的子查询,必须为外部查询的每一行重新计算 | SELECT * FROM t1 WHERE col = (SELECT col2 FROM t2 WHERE t2.id = RAND()) |
性能极差,尽量避免,考虑使用变量或JOIN重写 |
| UNCACHEABLE UNION | 属于不可缓存子查询的UNION中的第二个或更后面的SELECT | 包含RAND()、USER()等非确定性函数的UNION子查询 | 同UNCACHEABLE SUBQUERY,尽量避免使用非确定性函数在子查询中 |
性能较好的查询类型:
- SIMPLE:简单直接
- PRIMARY:主查询
- SUBQUERY:非依赖子查询
需要优化的查询类型:
- DEPENDENT SUBQUERY/DEPENDENT UNION:依赖外部查询,考虑JOIN重写
- DERIVED:派生表可能产生临时表
- UNCACHEABLE类型:无法缓存,性能最差
优化策略:
- 尽量将依赖子查询改写为JOIN
- 避免在子查询中使用非确定性函数
- 派生表过大时可考虑创建临时表或索引
(三)table - 表名
输出行所引用的表名。
该值也可以是以下选项之一:
- 实际表名:
users - 别名:
u - 派生表:
<derived2>(2是id值) - UNION结果:
<union1,2>
(四) partitions - 分区信息
查询将从中匹配记录的分区。对于非分区表,该值为NULL。
(五) type - 连接类型(非常重要)
从最优到最差排序:
| 类型 | 含义 | 性能 |
|---|---|---|
| system | 表只有一行记录(系统表) | 最优 |
| const | 通过主键或唯一索引一次就找到 | 非常快 |
| eq_ref | 唯一索引扫描,对于前表的每一行,后表只有一行匹配 | 非常快 |
| ref | 非唯一索引扫描,返回匹配某个单独值的所有行 | 快 |
| fulltext | 全文索引扫描 | 视情况而定 |
| ref_or_null | 类似ref,但包含NULL值的行 | 较快 |
| index_merge | 索引合并优化 | 视情况而定 |
| unique_subquery | 在IN子查询中使用唯一索引 | 较快 |
| index_subquery | 在IN子查询中使用非唯一索引 | 较快 |
| range | 索引范围扫描(BETWEEN、IN、>、<等) | 较好 |
| index | 全索引扫描(扫描索引树) | 一般 |
| ALL | 全表扫描 | 最差,需优化 |
1. ALL(全表扫描) - 最慢
- 原理:逐行检查表中的每一行数据
- 扫描行数:表中所有行
- 触发条件:
- 没有可用的索引
- 查询条件无法使用索引(如对索引列使用函数)
- 需要返回表中大部分数据时,优化器可能认为全表扫描更快
-- 没有索引的列进行查询
EXPLAIN SELECT * FROM users WHERE phone = '13800138000';
-- type: ALL
2. index(全索引扫描)
- 原理:扫描整个索引树,但只读取索引数据,不访问表数据
- 扫描行数:索引中的所有行
- 触发条件:
- 查询只使用索引中的列(覆盖索引)
ORDER BY子句与索引顺序一致
- 与ALL的区别:index扫描的是更小的索引结构,通常比全表扫描快
-- 假设在(name, age)上有复合索引
EXPLAIN SELECT name, age FROM users ORDER BY name;
-- type: index(使用索引排序,避免filesort)
3.range(范围扫描)
- 原理:使用索引查找一个范围内的值
- 扫描行数:索引中符合范围条件的行
- 触发条件:
- 使用
>、<、BETWEEN、IN、LIKE 'prefix%'等范围条件
- 使用
-- 假设在age上有索引
EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30;
-- type: range
4. ref(非唯一索引等值查询)
- 原理:使用非唯一索引进行等值比较,可能返回多行
- 扫描行数:索引中匹配值的行数
- 触发条件:
- 使用
=、<=>操作符 - 索引不是唯一索引(允许重复值)
- 使用
-- 假设在city上有普通索引(非唯一)
EXPLAIN SELECT * FROM users WHERE city = '北京';
-- type: ref
5.eq_ref(唯一索引等值查询)
- 原理:使用主键或唯一索引进行等值连接,最多返回一行
- 扫描行数:0或1行
- 触发条件:
- 多表连接时,连接条件使用主键或唯一索引
EXPLAIN SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id; -- u.id是主键
-- 对于users表的访问,type: eq_ref
6.const(常数引用) - 最快
- 原理:通过主键或唯一索引找到固定的一行数据
- 扫描行数:1行
- 触发条件:
- 使用主键或唯一索引的等值查询
- 查询结果最多只有一行
- 示例:
EXPLAIN SELECT * FROM users WHERE id = 1; -- id是主键
-- type: const
7. index_merge(索引合并)
- 同时使用多个索引,然后合并结果
- 示例:
WHERE key1 = 1 OR key2 = 2
8. unique_subquery(唯一子查询)
- 在
IN子查询中使用主键或唯一索引 - 示例:
WHERE id IN (SELECT user_id FROM ...)
9. index_subquery(索引子查询)
- 类似unique_subquery,但使用非唯一索引
(六) possible_keys - 可能使用的索引
possible_keys列表示MySQL可以从中选择查找此表中行的索引(不一定实际使用)。
请注意,此列完全独立于EXPLAIN输出中显示的表的顺序。这意味着possible_key中的一些key在实际中可能无法用于生成的表顺序。
如果此列为NULL(或在JSON格式的输出中未定义),则没有相关索引。
在这种情况下,您可以通过检查WHERE子句来检查它是否引用了适合索引的一个或多个列,从而提高查询的性能。
如果是这样,请创建一个适当的索引,并再次使用EXPLAIN检查查询。
(七) key - 实际使用的索引
key列表示MySQL实际决定使用的键(索引)。如果MySQL决定使用一个possible_keys索引来查找行,则该索引将作为键值列出。
key可以命名一个不在possible_keys值中的索引。如果所有可能的_keys索引都不适合查找行,但查询选择的所有列都是其他索引的列,则可能会发生这种情况。
也就是说,命名索引覆盖了选定的列,因此尽管它不用于确定要检索哪些行,但索引扫描比数据行扫描更有效。
对于InnoDB,即使查询也选择了主键,辅助索引也可能覆盖所选列,因为InnoDB将主键值与每个辅助索引一起存储。如果键为NULL,MySQL找不到用于更有效地执行查询的索引。
(八) key_len - 索引长度
表示索引中使用的字节数。
key_len列表示MySQL决定使用的键的长度。key_len的值使您能够确定MySQL实际使用的多部分密钥的多少部分。如果键列显示NULL,则key_len列也显示NULL。
由于密钥存储格式的原因,对于可以为NULL的列,密钥长度比NOT NULL列长一个。
计算规则:
-- TINYINT:1字节
-- SMALLINT:2字节
-- INT:4字节
-- BIGINT:8字节
-- DATE:3字节
-- TIMESTAMP:4字节
-- 字符集:utf8=3字节,utf8mb4=4字节
-- VARCHAR:额外2字节存储长度
(九)ref - 索引引用
显示索引的哪一列被使用了。
ref列显示了将哪些列或常量与键列中指定的索引进行比较,以从表中选择行。
如果值是func,则使用的值是某个函数的结果。要查看哪个函数,请使用EXPLAIN后的SHOW WARNINGS查看扩展的EXPLAIN输出。该函数实际上可能是一个运算符,如算术运算符。
示例:
const:常量值列名:某个表的列func:函数
(十)rows - 预估扫描行数
表示MySQL认为执行查询需要扫描的行数。(不是精确值)
优化目标:让这个值尽可能小
(十一) filtered - 过滤百分比
表示存储引擎返回的数据在server层过滤后,剩下的百分比。
filtered 列表示按表条件筛选的表行的估计百分比。
最大值为100,这意味着没有对行进行过滤。从100开始递减的值表示过滤量增加。
行显示了检查的估计行数,rows ×filtered过滤显示了与下表连接的行数。例如,如果rows为1000,filtered 为50.00(50%),则要与表连接的行数是1000×50%=500。
理想情况下应该是100%
(十二)Extra - 额外信息(非常重要)
EXPLAIN输出的Extra列包含有关MySQL如何解析查询的其他信息。以下列表解释了此列中可能出现的值。每个项目还指示JSON格式输出的哪个属性显示Extra值。
对于其中一些,有一个特定的属性。其他则显示为消息属性的文本。
如果你想让查询尽可能快,请注意Using filesort和Using temporary的额外列值,或者在JSON格式的EXPLAIN输出中,注意Using _filesort和Using _temporary_table属性等于true。
| 信息 | 含义 | 是否需优化 |
|---|---|---|
| Using index | 使用了覆盖索引(仅从索引获取数据) | 好现象 |
| Using where | 在存储引擎检索行后进行了过滤 | 视情况而定 |
| Using temporary | 使用了临时表(GROUP BY、ORDER BY等) | 通常需要优化 |
| Using filesort | 使用外部排序而不是索引排序 | 通常需要优化 |
| Using join buffer | 使用了连接缓存 | 可能需要优化 |
| Impossible WHERE | WHERE条件永远为false | 检查查询条件 |
| Select tables optimized away | 使用某些聚合函数优化 | 好现象 |
二、实际使用示例
案例一:全表扫描优化
1.问题描述
用户反馈一个简单的查询变得非常慢:
SELECT * FROM orders WHERE status = 'pending';
表中有100万条记录。
2.EXPLAIN分析
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1000000
filtered: 10.00
Extra: Using where
问题诊断:
type: ALL- 全表扫描key: NULL- 没有使用索引rows: 1M- 扫描了100万行
3.解决方案
-- 添加索引
ALTER TABLE orders ADD INDEX idx_status (status);
-- 再次EXPLAIN
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
优化后结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ref
possible_keys: idx_status
key: idx_status
key_len: 1023
ref: const
rows: 5000
filtered: 100.00
Extra: NULL
优化效果:
- 扫描行数从100万减少到5千
- 查询时间从2秒减少到0.02秒
案例二:覆盖索引优化
1.问题描述
分页查询很慢:
SELECT id, user_id, amount FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000, 20;
2.EXPLAIN分析
EXPLAIN SELECT id, user_id, amount FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000, 20;
结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ref
possible_keys: idx_status
key: idx_status
key_len: 1023
ref: const
rows: 200000
filtered: 100.00
Extra: Using where; Using filesort
问题诊断:
- 虽然用了索引,但是Extra列没有"Using index"
- 需要回表查询(先查索引,再查数据行)
- 排序需要临时表
3.解决方案
-- 创建覆盖索引
ALTER TABLE orders ADD INDEX idx_status_created_cover (status, created_at, id, user_id, amount);
-- 再次EXPLAIN
EXPLAIN SELECT id, user_id, amount FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 1000, 20;
优化后结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: range
possible_keys: idx_status_created_cover
key: idx_status_created_cover
key_len: 1031
ref: NULL
rows: 1020
filtered: 100.00
Extra: Using where; Using index
优化效果:
Extra: Using index- 使用了覆盖索引,无需回表- 查询时间从500ms减少到5ms
案例三:文件排序优化
1.问题描述
带ORDER BY的查询很慢:
SELECT * FROM products
WHERE category_id = 5
AND price > 100
ORDER BY sales_count DESC
LIMIT 50;
2.EXPLAIN分析
EXPLAIN SELECT * FROM products
WHERE category_id = 5
AND price > 100
ORDER BY sales_count DESC
LIMIT 50;
结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: products
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 50000
filtered: 1.00
Extra: Using where; Using filesort
问题诊断:
Using filesort- 外部排序,性能杀手- 全表扫描5万行
- 没有合适的索引
3.解决方案
-- 创建复合索引,注意列顺序
ALTER TABLE products ADD INDEX idx_category_price_sales (category_id, price, sales_count);
-- 再次EXPLAIN
EXPLAIN SELECT * FROM products
WHERE category_id = 5
AND price > 100
ORDER BY sales_count DESC
LIMIT 50;
优化后结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: products
partitions: NULL
type: range
possible_keys: idx_category_price_sales
key: idx_category_price_sales
key_len: 8
ref: NULL
rows: 1500
filtered: 100.00
Extra: Using where; Backward index scan
优化效果:
- 移除了
Using filesort - 扫描行数从5万减少到1500
- 使用了反向索引扫描(DESC排序优化)
- 查询时间从800ms减少到10ms
案例四:子查询优化
1.问题描述
子查询性能差:
SELECT u.* FROM users u
WHERE u.id IN (
SELECT user_id FROM orders
WHERE created_at > '2024-01-01'
AND amount > 1000
);
2.EXPLAIN分析
EXPLAIN SELECT u.* FROM users u
WHERE u.id IN (
SELECT user_id FROM orders
WHERE created_at > '2024-01-01'
AND amount > 1000
);
结果:
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: u
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 10000
filtered: 100.00
Extra: Using where
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: orders
partitions: NULL
type: index_subquery
possible_keys: idx_user_id
key: idx_user_id
key_len: 8
ref: func
rows: 5
filtered: 11.11
Extra: Using where
问题诊断:
select_type: DEPENDENT SUBQUERY- 依赖子查询- 对外部查询的每一行都执行子查询
- 时间复杂度:O(n²)
解决方案
-- 方案1:使用JOIN重写
EXPLAIN SELECT DISTINCT u.*
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at > '2024-01-01'
AND o.amount > 1000;
-- 方案2:使用EXISTS
EXPLAIN SELECT u.* FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.created_at > '2024-01-01'
AND o.amount > 1000
);
优化后EXPLAIN结果(JOIN版本):
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: o
partitions: NULL
type: range
possible_keys: idx_user_created_amount
key: idx_user_created_amount
key_len: 8
ref: NULL
rows: 2000
filtered: 33.33
Extra: Using where; Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: u
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: db.o.user_id
rows: 1
filtered: 100.00
Extra: NULL
优化效果:
- 消除了依赖子查询
- 使用了覆盖索引
- 查询时间从5秒减少到0.05秒
案例五:联合索引最左前缀原则
1.问题描述
已存在索引idx_user_action (user_id, action_time) ,单索引没有被使用:
SELECT * FROM logs
WHERE action_time > '2024-01-01'
AND user_id = 100;
EXPLAIN SELECT * FROM logs
WHERE action_time > '2024-01-01';
2.EXPLAIN分析
EXPLAIN SELECT * FROM logs
WHERE action_time > '2024-01-01'
AND user_id = 100;
结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: logs
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 100000
filtered: 33.33
Extra: Using where
问题诊断:
- 缺少索引最左列
user_id - 导致全表扫描
3.解决方案
正确使用索引,因为条件中有user_id(索引最左列)。
# 调整顺序
EXPLAIN SELECT * FROM logs
WHERE user_id = 100
AND action_time > '2024-01-01';
-- 如果需要单独按action_time查询,需要额外索引
ALTER TABLE logs ADD INDEX idx_action_time (action_time);
-- 或者调整查询条件,使用复合索引
EXPLAIN SELECT * FROM logs
WHERE user_id IN (SELECT DISTINCT user_id FROM logs WHERE action_time > '2024-01-01');
结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: logs
partitions: NULL
type: range
possible_keys: idx_user_action
key: idx_user_action
key_len: 12
ref: NULL
rows: 500
filtered: 100.00
Extra: Using index condition
案例六:临时表优化
1.问题描述
GROUP BY查询很慢:
SELECT category_id, COUNT(*) as cnt, AVG(price) as avg_price
FROM products
WHERE status = 'active'
GROUP BY category_id
ORDER BY cnt DESC;
2.EXPLAIN分析
EXPLAIN SELECT category_id, COUNT(*) as cnt, AVG(price) as avg_price
FROM products
WHERE status = 'active'
GROUP BY category_id
ORDER BY cnt DESC;
结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: products
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 50000
filtered: 50.00
Extra: Using where; Using temporary; Using filesort
问题诊断:
Using temporary- 使用了临时表Using filesort- 使用了文件排序- 双重性能问题
3.解决方案
-- 创建合适的索引
ALTER TABLE products ADD INDEX idx_status_category (status, category_id, price);
-- 或者调整SQL写法(MySQL 8.0+)
-- 使用窗口函数或者物化视图
-- 再次EXPLAIN
EXPLAIN SELECT category_id, COUNT(*) as cnt, AVG(price) as avg_price
FROM products
WHERE status = 'active'
GROUP BY category_id
ORDER BY cnt DESC;
优化后结果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: products
partitions: NULL
type: ref
possible_keys: idx_status_category
key: idx_status_category
key_len: 1023
ref: const
rows: 25000
filtered: 100.00
Extra: Using index; Using temporary; Using filesort
注意:虽然还有临时表,但已经使用了索引,性能有所提升。
案例七:连接顺序优化
1.问题描述
多表连接查询慢:
SELECT u.name, o.order_no, p.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE u.city = '北京'
AND o.status = 'completed'
AND p.category_id = 3;
2.EXPLAIN分析
EXPLAIN SELECT u.name, o.order_no, p.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE u.city = '北京'
AND o.status = 'completed'
AND p.category_id = 3;
结果(简化):
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: p
partitions: NULL
type: ref
possible_keys: idx_category
key: idx_category
key_len: 5
ref: const
rows: 500
filtered: 100.00
Extra: NULL
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: oi
partitions: NULL
type: ref
possible_keys: idx_product
key: idx_product
key_len: 8
ref: db.p.id
rows: 10
filtered: 100.00
Extra: Using where
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: o
partitions: NULL
type: eq_ref
possible_keys: PRIMARY,idx_user_status
key: PRIMARY
key_len: 8
ref: db.oi.order_id
rows: 1
filtered: 10.00
Extra: Using where
*************************** 4. row ***************************
id: 1
select_type: SIMPLE
table: u
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 8
ref: db.o.user_id
rows: 1
filtered: 10.00
Extra: Using where
分析:
- MySQL自动选择了最优的连接顺序(从products表开始)
- 每次连接都使用了合适的索引
如果连接顺序不佳,可以强制指定
-- 使用STRAIGHT_JOIN强制连接顺序
EXPLAIN SELECT u.name, o.order_no, p.product_name
FROM users u
STRAIGHT_JOIN orders o ON u.id = o.user_id
STRAIGHT_JOIN order_items oi ON o.id = oi.order_id
STRAIGHT_JOIN products p ON oi.product_id = p.id
WHERE u.city = '北京'
AND o.status = 'completed'
AND p.category_id = 3;
总结
实战总结表
| 问题现象 | EXPLAIN关键指标 | 解决方案 | 效果提升 |
|---|---|---|---|
| 查询慢,扫描大量数据 | type=ALL, rows数值大 | 添加WHERE条件索引 | 10-1000倍 |
| 排序慢 | Extra=Using filesort | 添加ORDER BY字段的复合索引 | 50-500倍 |
| 分组慢 | Extra=Using temporary | 优化GROUP BY,添加索引 | 20-200倍 |
| 子查询慢 | select_type=DEPENDENT SUBQUERY | 改写为JOIN | 100-1000倍 |
| 分页慢 | rows数值大,无覆盖索引 | 使用覆盖索引 | 10-100倍 |
| 索引未生效 | key=NULL | 检查最左前缀原则 | 视情况而定 |
| 多表连接慢 | 连接顺序不佳 | 优化连接顺序,添加关联索引 | 5-50倍 |
实用EXPLAIN技巧
-- 1. 查看更详细的信息
EXPLAIN FORMAT=JSON SELECT ...;
-- 2. 查看实际执行成本(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT ...;
-- 3. 比较不同索引的效果
EXPLAIN SELECT ... USE INDEX (index1);
EXPLAIN SELECT ... USE INDEX (index2);
-- 4. 查看优化器追踪
SET optimizer_trace="enabled=on";
SELECT ...;
SELECT * FROM information_schema.optimizer_trace;
-- 5. 使用PERFORMANCE_SCHEMA监控
SELECT * FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text LIKE '%SELECT * FROM orders%';

更多推荐


所有评论(0)