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类型:无法缓存,性能最差

优化策略:

  1. 尽量将依赖子查询改写为JOIN
  2. 避免在子查询中使用非确定性函数
  3. 派生表过大时可考虑创建临时表或索引

(三)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(范围扫描)

  • 原理:使用索引查找一个范围内的值
  • 扫描行数:索引中符合范围条件的行
  • 触发条件
    • 使用><BETWEENINLIKE '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 filesortUsing temporary的额外列值,或者在JSON格式的EXPLAIN输出中,注意Using _filesortUsing _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%';

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐