mysql用explain查看执行计划和索引测试

当你执行 EXPLAIN SELECT … 时,MySQL 内部的查询优化器 (Query Optimizer) 会开始工作。它会分析你的 SQL,评估多种可能的执行路径(比如,使用索引 A、使用索引 B、或者干脆全表扫描),并基于一个成本模型 (Cost Model) 来选择它认为总成本最低的那个计划。EXPLAIN 就是将这个最终选定的计划以表格形式展示给你。

1. explain

1.1 字段解释

列名解释
id查询中操作的序列号。id 越大,越先执行;id 相同,则从上到下执行。
select_type查询的类型(SIMPLE:简单查询,SUBQUERY:子查询,UNION:联合查询等)。
table这一步操作正在访问的表。
type【最重要】 访问类型,显示了查找数据的方式。从最好到最差依次是: system > const > eq_ref > ref > range > index > ALLALL 代表全表扫描,通常是优化的首要目标。
possible_keysMySQL 认为“可能”可以用于这个查询的索引列表。
keyMySQL 最终“实际决定”使用的索引。如果为 NULL,则表示没有使用索引。
rowsMySQL 估算要读取并检查才能找到所需结果的行数。这是个估算值,但非常关键。原则上 rows 越少越好。
filtered一个百分比,表示 WHERE 条件过滤后,剩下的数据行占 rows 的比例。
Extra【第二重要】 包含额外但至关重要的信息。比如 Using index (使用了覆盖索引,很棒!), Using where (需要回表查询), Using temporary (使用了临时表), Using filesort (进行了文件排序,性能杀手)。

1.2 注意事项

  • 定期 ANALYZE TABLEEXPLAIN 的估算依赖于表的统计信息。定期运行 ANALYZE TABLE your_table; 可以更新这些信息,让优化器做出更准确的判断。
  • 过度关注 type,而忽略了 Extra 列。type 固然重要,但 Extra 列里的 Using filesortUsing temporary 是两大性能杀手。一个查询即使 typerange(还不错),但如果伴随着 filesort,其性能可能依然很糟糕。
  • rows 列的数字越小越好,基本正确,但要结合 filtered 列看。如果 rows 很大,但 filtered 很低(比如 1%),说明 WHERE 条件过滤掉了大部分数据,对下一个 JOIN 的表的驱动行数就很少,整体性能可能不差。

1.3 提问问题

  • 当你用 EXPLAIN 分析一条 SELECT 语句时,发现 possible_keys 列显示了一个理想的索引 idx_user_id,但 key 列却是 NULL,并且 typeALL。请问,除了“表数据量太小”这个原因外,还可能存在哪两种情况导致了优化器做出这个“糟糕”的决定?

    • 索引选择性(Selectivity)过差:优化器通过统计信息判断,使用这个索引需要扫描的行数占了总行数的很大比例(例如,超过 20%-30%)。在这种情况下,它认为直接进行全表扫描(一次顺序 I/O)比先读索引(随机 I/O)再回表查询大部分数据(更多随机 I/O)的综合成本更低。

    • 查询条件导致索引失效:查询语句的写法本身阻止了优化器使用索引。最典型的情况是:对索引列使用函数或运算和发生隐式类型转换。

  • EXPLAINEXPLAIN ANALYZE 的核心区别是什么?请描述一个你必须使用 EXPLAIN ANALYZE 才能确诊的性能问题场景。

    • EXPLAIN 是计划书(告诉你“我打算怎么做”),它不执行查询,只返回优化器的估算。而 EXPLAIN ANALYZE 是带行车记录仪的导航(告诉你“我不但计划好了,还实际跑了一遍,这是真实记录”),它会实际执行查询,并返回包含真实执行时间、真实返回行数的详细计划。当 EXPLAIN 的估算严重失准,使用 EXPLAIN ANALYZE
  • Extra 列中,你看到了 Using index; Using filesort。这代表了什么看似矛盾的情况?这样的查询性能究竟是好是坏?

    • 它意味着从索引中获取数据后,得到的结果集顺序不满足 ORDER BY 子句的要求,因此 MySQL 必须在内存或临时磁盘文件上进行一次额外的排序操作。
    • 它通常比 Using where; Using filesort 要好。因为 Using index 避免了回表查询,大大减少了数据读取量。
    • 调整索引,使其顺序与 ORDER BY 一致。

2. 具体类型

2.1 type

从最优到最差排序:

  • system: 表只有一行数据(系统表),是 const 的特例。几乎不会出现。
  • const: 通过主键或唯一索引,最多只匹配一行数据。速度极快。
    • explain select * from user_info where id = 2
  • eq_ref: 在 JOIN 中,驱动表(前一张表)的每一行,在被驱动表(当前表)中都只通过主键或唯一索引匹配到一行数据。是 JOIN 的最佳类型。
    • EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id
  • ref: 非唯一性索引扫描,返回匹配某个单独值的所有行。例如,基于普通索引的等值查询。
  • fulltext: 全文索引。
  • ref_or_null: 类似 ref,但会额外搜索 NULL 值的行。
  • index_merge: 查询使用了多个单列索引,然后将结果合并。
  • unique_subquery: IN 子查询中,子查询返回的是唯一值。
  • range: 使用索引进行范围查询。例如 BETWEEN, >, <, IN() 等。
  • index: 全索引扫描。和 ALL 类似,但只扫描索引树,不扫描表数据。如果 Extra 中有 Using index,表示是覆盖索引,性能尚可。否则表示需要回表,性能较差。
  • ALL: 全表扫描 (Full Table Scan)。遍历整张表来查找匹配的行,是性能最差的情况,是优化的首要目标。

2.2 key_len

索引使用长度(字节)。表示本次查询实际用到了索引的多少个字节。你可以根据这个值判断联合索引是否被完全利用。

  • VARCHAR(N): N * 字符集字节数 + 2 字节。

  • INT: 4 字节。

  • BIGINT: 8 字节。

  • 如果字段允许为 NULL,则需要额外 +1 字节。

联合索引场景

完全匹配, 就是 key_len 是满的, 不完全匹配, 例如前面, key_len 就是少的

例如: (user_id, award_id, lottery_id) 复合索引, 一共是12

1
2
3
4
5
6
7
8
-- key_len:4
explain SELECT * FROM `lottery_user_log` WHERE (user_id=279223)

-- key_len:8
explain SELECT * FROM `lottery_user_log` WHERE (user_id=279223 and award_id = 8)

-- key_len:12
explain SELECT * FROM `lottery_user_log` WHERE (user_id=279223 and award_id = 8 and lottery_id=1)

字符串的 key_len

1
2
3
4
5
6
7
8
9
CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT,
`city` VARCHAR(50) NOT NULL,
`name` VARCHAR(50) NOT NULL,
`age` INT NOT NULL,
`phone` VARCHAR(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4;

  • 普通索引
1
2
3
4
5
ALTER TABLE `users` ADD INDEX `idx_city` (`city`);
EXPLAIN SELECT * FROM `users` WHERE `city` = '北京';

长度 = 50 (字符数) * 4 (utf8mb4) + 2 (变长存储) = 202。
所以,你会看到 key_len: 202。
  • 联合索引与最左前缀原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 查询1
ALTER TABLE `users` ADD INDEX `idx_city_name_age` (`city`, `name`, `age`);

EXPLAIN SELECT * FROM `users` WHERE `city` = '上海';
key_len 计算: 只用到了 city。结果和场景一相同,key_len: 202。

-- 查询2
EXPLAIN SELECT * FROM `users` WHERE `city` = '上海' AND `name` = '张三';
city 部分: 50 * 4 + 2 = 202
name 部分: 50 * 4 + 2 = 202
总长度 = 202 + 202 = 404。

-- 查询3
EXPLAIN SELECT * FROM `users` WHERE `city` = '上海' AND `age` = 30;
结果仍然是 key_len: 202。这个结果告诉你,你的联合索引没有被充分利用。

key_len 是诊断联合索引使用情况最直接、最精确的工具。看到一个远小于预期的 key_len 值,就意味着你的查询没能有效利用联合索引的后续部分,需要检查 WHEREJOIN 条件是否遵循了最左前缀原则。

2.3 extra

A. 绿色信号 (好消息)

  • Using index: 覆盖索引。查询所需的所有列都可以在这个索引中找到,因此存储引擎无需再访问数据表(即“回表”)。这是极高的性能优化,尤其是在 COUNT(*) 或只查询索引列时。
  • Using index condition (ICP): 索引下推。这是一个重要的优化。它允许存储引擎在使用索引检索行时,就用 WHERE 子句中其他条件(无法用于索引定位,但其列包含在索引中)来过滤数据。这减少了从存储引擎传回给 Server 层的行数,降低了 I/O 和 CPU 消耗。
  • Select tables optimized away: 查询可以直接从索引的元信息或聚合函数中得到结果,根本不需要访问表。例如 SELECT MIN(id) FROM users (id 是主键)。
  • No tables used: 查询不涉及任何表,例如 SELECT NOW(); 或者 SELECT 1;

B. 黄色信号 (需警惕)

  • Using where

    这是一个非常常见的信息。它表示存储引擎返回数据后,MySQL Server 层需要对这些行再次应用WHERE子句进行过滤。

    • 它本身不是坏事,但它提示你:当前索引无法覆盖所有的 WHERE 条件。
    • Using index 结合: Using index; Using where 表示使用了覆盖索引,但索引中的数据仍需进一步过滤。性能不错。
    • 单独出现: 表示发生了回表,然后在 Server 层进行过滤。你需要思考:是否可以通过创建更合适的索引来消除 Using where 或将其转变为 Using index condition
  • Using join buffer (Block Nested Loop / Batched Key Access): 当两个表进行 JOIN 时,如果关联字段在被驱动表上没有索引,MySQL 不得不使用一块内存区域(Join Buffer)来缓存驱动表的行,然后扫描整个被驱动表进行匹配。这是极其低效的 JOIN 方式,看到它就意味着必须为 JOIN 字段在被驱动表上创建索引。

  • const query: 查询被优化为常量,只执行一次。

C. 红色信号 (严重性能问题)

  • Using filesort

    :文件排序。这是性能的主要杀手之一。它意味着查询结果的排序无法利用任何索引的天然顺序,MySQL 必须在内存中(如果结果集小)或在磁盘上创建临时文件(如果结果集大)来进行一次额外的排序操作。这会消耗大量 CPU 和 I/O。

    • 修复办法: 调整或创建联合索引,使其列顺序与 ORDER BY 子句的顺序和方向一致。
  • Using temporary

    :使用临时表。这是另一个主要杀手。通常发生在GROUP BYDISTINCT操作中,当优化器无法使用索引来完成这些操作时,它需要创建一个内部临时表来存放中间结果。

    • 内存临时表:性能尚可,但消耗内存。
    • 磁盘临时表:如果临时表过大(超过 tmp_table_sizemax_heap_table_size),会转为磁盘存储,性能会急剧下降。
    • 修复办法: 优化 GROUP BY 的字段,确保它们在同一个索引中且顺序正确。
  • Impossible WHERE: WHERE 子句的条件逻辑上永远为 FALSE。这通常指示查询本身存在逻辑错误,而不是性能问题,但 EXPLAIN 仍会帮你找出来。

2.4 rows 和 filtered

rows:估算的扫描行数,但它只是一个估算,可能会不准。

filtered : 它表示在 rows 数量的记录中,经过 WHERE 子句中其他条件的过滤后,预计还剩下多少比例的记录(百分比)

1
2
3
4
5
6
7
EXPLAIN SELECT * FROM users WHERE age > 25;
- 如果 rows = 1000,表示 MySQL 估计需要检查 1000 行来找到所有 age > 25 的记录。

EXPLAIN SELECT * FROM users WHERE age > 25 AND city = 'Beijing';
- rows = 1000(扫描 1000 行)
- filtered = 10(其中 10% 满足条件)
- 实际返回约 100 行(1000 × 10%)

2.5 提问问题

  • 为什么 range 比 index 性能高?

    • range: 扫描的是索引的子集。工作量 = 范围大小
    • index: 扫描的是索引的全部。工作量 = 整个索引大小

    因此,只要 range 的范围不是覆盖整个索引,它的性能就必然优于 index

  • ref 和 index 的区别是什么?

    • ref 是基于等值条件的精确查找,而 index 是无差别地扫描整个索引树。
    • 当你使用非唯一索引或联合索引的最左前缀进行等值查询 (=) 时,就会出现 ref
    • index 是对索引的地毯式轰炸,它没有利用索引进行快速定位的优势,只是简单地把索引当成一个比主表小的数据文件来遍历。

3. 索引测试查看

3.1 数据初始化

1
2
docker run -d --name mysql-explain -e MYSQL_ROOT_PASSWORD=123456 mysql # 创建一个容器
docker exec -it mysql-explain mysql -u root -p # 输入密码, 进入操作

新建测试表,插入 10w 数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 创建库
CREATE DATABASE test1;
use test1;

-- 创建表
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

-- 批量插入 10w 数据
DROP PROCEDURE IF EXISTS batchInsert;
DELIMITER $
CREATE PROCEDURE batchInsert() BEGIN DECLARE i INT DEFAULT 1;
START TRANSACTION; WHILE i<=100000
DO
INSERT INTO test (a,b) VALUES (i,i);
SET i=i+1; END WHILE;
COMMIT; END $

CALL batchInsert();


-- 得到100000
select count(*) from test;

3.2 单个索引

  • 全表扫描 ALL
1
explain select * from test;  
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrapartitionsfiltered
1SIMPLEtestALLNULLNULLNULLNULL100098NULLNULL100.00

其中 type 值为 ALL,表示全表扫描了,我们看到 rows 这个字段显示有 100098 条,实际上我们一共才 10w 条数据,说明这个字段只是 mysql 的一个预估,不总是准确的。

  • ref
1
2
3
alter table test add index idx_a(a);  

explain select * from test where a = 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrefidx_aidx_a4const1NULL100
  • 全表扫描 ALL,Using where
1
2
3
-- 因为这条语句会从索引中查出 9w 条数据,全表扫描都才 10w 条数据,所以 mysql 决策是还不如直接全表扫描得了。

explain select * from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestALLidx_aNULLNULLNULL100098Using where50.00
  • range
1
2
3
-- 使用了 a 索引树, 因为满足索引只有 10000 条数据,mysql 认为 10000 条数据就算回表也要比全表扫描的代价低,因而决定查索引,但还是需要回表。

explain select * from test where a > 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_aidx_a4NULL10000Using index condition100.00
  • range 不回表
1
2
3
-- 只select 索引字段, 注意这次 Extra 的值为 `Using where; Using index`,表示查询用到了索引,且要查询的字段在索引中就能拿到,所以不需要回表。
-- 显然这种效率比上面的要高,这也是日常开发中不建议写 select * 的原因,尽量只查询业务所需的字段。
explain select a from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_aidx_a4NULL50049Using where; Using index100.00

3.3 order by

  • range 不回表
1
2
-- 走 a 索引树, 不回表
explain select a from test where a > 90000 order by a;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_aidx_a4NULL10000Using where; Using index100.00
  • range 回表
1
2
3
-- 走 a 索引树, 需要回表
explain select * from test where a > 90000 order by a;
explain select b from test where a > 90000 order by a;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_aidx_a4NULL10000Using index condition100.00
  • range + 内存排序
1
2
3
-- Extra中返回了一个 Using filesort,说明无法利用索引完成排序,需要从内存或磁盘进行排序。就算 b 有索引, 也无法避免, 因为也会走 a 的索引树。

explain select a from test where a > 90000 order by b;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_aidx_a4NULL10000Using index condition; Using filesort100.00

3.4 多个字段索引

目前a, b 有自己的单个索引。

1
2
3
alter table test drop index idx_a_b;
alter table test add index idx_a(a);
alter table test add index idx_b(b);
  • 没走索引,是因为无论如何都需要回表, 如果把10000改成90000, 会变成走索引加回表.
1
2
3
explain select * from test where a > 10000;
explain select a,b from test where a > 10000;
explain select b from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestALLidx_aNULLNULLNULL100098Using where50
  • 直接走a 的索引树, 不回表
1
explain select a from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_aidx_a4NULL50049Using where; Using index100
  • 走 b 的索引树更好,回表
1
explain select a,b from test where a > 90000 and b = 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrefidx_a,idx_bidx_b4const1Using where9.99
  • 走 a 的索引树更好,回表
1
explain select a,b from test where a = 90000 and b > 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrefidx_a,idx_bidx_a4const1Using where9.99

3.5 复合索引

一个复合索引,(a,b)复合索引

1
2
3
alter table test drop index idx_a;
alter table test drop index idx_b;
alter table test add index idx_a_b(a,b);
  • 走 (a,b) 的索引树, 不回表
1
2
3
4
explain select * from test where a > 10000;
explain select a,b from test where a > 10000;
explain select b from test where a > 10000;
explain select a from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_a_bidx_a_b4NULL50049Using where; Using index100
  • 不满足最左匹配原则
1
explain select * from test where b > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestindexNULLidx_a_b8NULL100098Using where; Using index33.33
  • 走 (a,b) 的索引树, 不回表, key_len=4
1
explain select a,b from test where a > 90000 and b = 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_a_bidx_a_b4NULL18142Using where; Using index10
  • 走 (a,b) 的索引树, 不回表, key_len=8
1
explain select a,b from test where a = 90000 and b > 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_a_bidx_a_b8NULL1Using where; Using index100

普通索引和复合索引同时存在(2个普通索引,1个复合索引。)

1
2
3
alter table test add index idx_a(a); 
alter table test add index idx_b(b);
alter table test add index idx_a_b(a,b);
  • 走 (a,b) 的索引树更好, 不回表
1
2
3
explain select * from test where a > 10000;
explain select a,b from test where a > 10000;
explain select b from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_a_b,idx_aidx_a_b4NULL50049Using where; Using index100
  • 走 a 的索引树更好, 不回表
1
explain select a from test where a > 10000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_a_b,idx_aidx_a4NULL50049Using where; Using index100
  • 走 b 的索引树, 需要回表. 其实此时走复合索引更好, 所以不建议复合索引和普通索引一起用
1
explain select a,b from test where a > 90000 and b = 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrefidx_a_b,idx_a,idx_bidx_b4const1Using where18.12
  • 走 (a,b) 的索引树, 不回表
1
explain select a,b from test where a = 90000 and b > 90000;
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtrafiltered
1SIMPLEtestrangeidx_a_b,idx_a,idx_bidx_a_b8NULL1Using where; Using index100

4. 提问问题

  • mysql explain 的 rows 和 filtered 怎么理解,都是越小越好吗?

    • rows 越小通常越好。这好比图书管理员告诉你:“根据你的条件,我只需要翻看 10 本书就能找到你要的。” 这显然比“我得翻 100 万本”要高效得多。一个小的 rows 值通常意味着你的索引利用得很好。

    • filtered: 扫描后留下的书的比例。

      • filtered 越大越好(越接近 100% 越好)。这好比图书管理员在翻看了 10 本书(rows=10)之后,发现这 10 本书 100% 都是你想要的(filtered=100%)。这说明索引的筛选非常精准。

      • 反之,如果 rows=100万,而 filtered=0.01%,这意味着 MySQL 辛辛苦苦扫描了 100 万行数据,但其中 99.99% 都是无用功,最后只留下了区区 100 行。这是一个强烈的危险信号,说明当前索引对于整个查询条件的过滤效果很差。

  • 单词查询一次可以用多个索引吗?

    • 通常情况下,一个表在一次查询中只会使用一个最优索引。但有一种特殊情况可以,它叫做“索引合并(Index Merge)”。
    • SELECT * FROM users WHERE phone = '138...' OR email = 'test@example.com'; (假设 phoneemail 分别有独立索引)
  • 不满足最左匹配,什么场景下也可以使用到索引?

    • 索引覆盖 (Covering Index)
      • idx_city_age_name (city, age, name)
      • SELECT name, age FROM users WHERE age = 30;
    • 索引下推 (Index Condition Pushdown, ICP)
      • idx_name_age (name, age)
      • SELECT * FROM staff WHERE name LIKE ‘张%’ AND age = 25;

5. 参考资料