MySQL进阶篇之SQL性能分析示例详解
作者:bksczm
准备测试表与数据
-- 班级表
CREATE TABLE class (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL
) ENGINE=InnoDB;
-- 学生表:主键 id;name 普通索引;class_id 普通索引;(name,age) 复合索引;age/email 无独立索引
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
class_id INT,
email VARCHAR(50),
INDEX idx_name (name),
INDEX idx_class_id (class_id),
INDEX idx_name_age (name, age)
) ENGINE=InnoDB;
-- 文章表:全文索引(ngram 支持中文分词)
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200),
content TEXT,
FULLTEXT INDEX ft_title_content (title, content) WITH PARSER ngram
) ENGINE=InnoDB;
INSERT INTO class(name) VALUES ('一班'),('二班'),('三班');
INSERT INTO student(name, age, class_id, email) VALUES
('张三', 18, 1, 'zs@xx.com'),
('张三', 20, 1, 'zs2@xx.com'),
('李四', 19, 2, 'ls@xx.com'),
('王五', 21, 2, NULL),
(NULL, 22, 3, NULL);
INSERT INTO articles(title, content) VALUES
('MySQL索引教程','讲解数据库索引和B+树'),
('事务隔离详解','讲解MVCC和ReadView');
准备:EXPLAIN 概述
语法与作用
EXPLAIN SQL语句;
EXPLAIN 返回 MySQL 优化器打算如何执行这条 SQL 的执行计划,而不是盲目执行后再看结果。通过分析执行计划,可以判断查询是否走了索引、扫描了多少行、是否有临时表 / 文件排序等性能问题。
是否真正执行
- 普通
EXPLAIN SELECT ...:不会真正执行查询,只返回执行计划(但优化器可能做一些统计和预处理); EXPLAIN INSERT/UPDATE/DELETE:不会真正修改数据;EXPLAIN ANALYZE(MySQL 8.0.18+):会真正执行 SQL,并返回实际执行的耗时、行数等信息,比普通 EXPLAIN 更精确。
一、id(执行顺序)
1.1 id 相同:从上到下顺序执行(表连接)
EXPLAIN SELECT * FROM student s INNER JOIN class c ON s.class_id = c.id;
+----+--------+-------+--------+---------------+---------+ | id | select_type | table | type | key | ... | +----+--------+-------+--------+---------------+---------+ | 1 | SIMPLE | c | ALL | PRIMARY | ... | ← 先扫描 class | 1 | SIMPLE | s | ref | idx_class_id | ... | ← 再用 class_id 关联 student +----+--------+-------+--------+---------------+---------+
两行 id 都是 1,属于同一层级,从上到下执行。
1.2 id 不同:id 越大越先执行(子查询)
EXPLAIN SELECT * FROM student WHERE class_id = (SELECT id FROM class WHERE name = '一班');
+----+-------------+---------+-------+ | id | select_type | table | type | +----+-------------+---------+-------+ | 1 | PRIMARY | student | ref | ← 外层后执行 | 2 | SUBQUERY | class | const | ← id=2 更大,子查询先执行 +----+-------------+---------+-------+
1.3 id 为 NULL:UNION 结果集,最后执行
EXPLAIN SELECT id FROM student WHERE id < 3 UNION SELECT id FROM student WHERE id > 4;
+------+--------------+------------+------+ | id | select_type | table | type | +------+--------------+------------+------+ | 1 | PRIMARY | student | range| | 2 | UNION | student | range| | NULL | UNION RESULT | <union1,2> | ALL | ← 最后合并结果 +------+--------------+------------+------+
二、select_type(查询类型)
2.1 SIMPLE:简单查询
EXPLAIN SELECT * FROM student WHERE id = 1; -- select_type: SIMPLE(无子查询、无 UNION)
2.2 PRIMARY + SUBQUERY:外层 + 非相关子查询
EXPLAIN SELECT * FROM student WHERE class_id = (SELECT id FROM class WHERE name = '一班'); -- 外层: PRIMARY 子查询: SUBQUERY
2.3 DEPENDENT SUBQUERY:相关子查询(依赖外层)
EXPLAIN SELECT * FROM student s WHERE EXISTS (SELECT 1 FROM class c WHERE c.id = s.class_id);
| 1 | PRIMARY | s | ... | | 2 | DEPENDENT SUBQUERY | c | ... | ← 子查询引用了外层的 s.class_id
2.4 DERIVED:FROM 子句中的派生表
EXPLAIN SELECT t.name FROM (
SELECT id, name FROM student WHERE age > 18
) t WHERE t.id = 1;
| 1 | PRIMARY | <derived2> | ... | ← 派生表 | 2 | DERIVED | student | ... | ← 先物化成临时表
2.5 UNION / UNION RESULT
EXPLAIN SELECT id FROM student WHERE id < 3 UNION SELECT id FROM student WHERE id > 4; -- 第一个 SELECT: PRIMARY -- 第二个 SELECT: UNION -- 合并行: UNION RESULT(id=NULL)
2.6 MATERIALIZED:物化子查询
EXPLAIN SELECT * FROM student WHERE class_id IN (SELECT id FROM class WHERE id > 1); -- 子查询结果被物化成临时表,select_type 可能显示 MATERIALIZED
三、table 与 partitions
3.1 table:涉及的表
EXPLAIN SELECT * FROM student s, class c WHERE s.class_id = c.id; -- table: s / c(显示表名或别名) -- UNION 结果: <union1,2>;派生表: <derived2>
3.2 partitions:匹配的分区
-- 未分区表
EXPLAIN SELECT * FROM student WHERE id = 1;
-- partitions: NULL
-- 分区表示例(按 RANGE 分区)
CREATE TABLE log_part (
id INT, create_time DATE
) PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);
EXPLAIN SELECT * FROM log_part WHERE YEAR(create_time) = 2024;
-- partitions: p2024(只扫描命中的分区,即分区裁剪)
partitions 显示分区名;未分区表为 NULL。
四、type(访问类型)⭐
性能从好到差:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
4.1 system:表中只有一行
CREATE TABLE t_one (id INT PRIMARY KEY) ENGINE=MyISAM; INSERT INTO t_one VALUES (1); EXPLAIN SELECT * FROM t_one; -- type: system
InnoDB 单行表通常显示 const;system 多见于 MyISAM/MEMORY 单行表或系统表。
4.2 const:主键/唯一索引等值匹配常量(最终结果只会出现单行)
EXPLAIN SELECT * FROM student WHERE id = 1; -- type: const key: PRIMARY rows: 1
4.3 eq_ref:被驱动表走主键/唯一索引等值关联(子查询也会存在)
EXPLAIN SELECT s.name, c.name FROM student s INNER JOIN class c ON s.class_id = c.id;
| table | type | key | ref | | c | ALL | NULL | NULL | ← 驱动表全扫描(数据少) | s | eq_ref | PRIMARY | test.c.id | ← 每行用 c.id 主键精确匹配,最多一条
4.4 ref:普通索引等值查询,可能多行
EXPLAIN SELECT * FROM student WHERE name = '张三'; -- type: ref key: idx_name ref: const rows: 2
name 是普通索引(非唯一),有两个"张三",返回多行。
4.5 fulltext:全文索引
EXPLAIN SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('索引');
-- type: fulltext key: ft_title_content
必须用
MATCH ... AGAINST,用=或LIKE不会走全文索引。
4.6 ref_or_null:等值 + IS NULL
EXPLAIN SELECT * FROM student WHERE name = '王五' OR name IS NULL; -- type: ref_or_null key: idx_name
WHERE 中显式写了
IS NULL才出现;列允许 NULL 但不查 NULL 仍是 ref。
4.7 index_merge:多个索引合并
EXPLAIN SELECT * FROM student WHERE id = 1 OR name = '张三'; -- type: index_merge -- key: PRIMARY,idx_name -- Extra: Using union(PRIMARY,idx_name); Using where
4.8 unique_subquery:IN 子查询返回主键/唯一键
-- MySQL 5.6+ 默认 semi-join 会改写,先关闭才能看到 SET optimizer_switch = 'semijoin=off'; EXPLAIN SELECT * FROM student WHERE class_id IN (SELECT id FROM class WHERE name = '一班'); -- 子查询 class.id 是主键 → type: unique_subquery SET optimizer_switch = 'semijoin=on'; -- 恢复
4.9 index_subquery:IN 子查询返回普通索引列
SET optimizer_switch = 'semijoin=off'; EXPLAIN SELECT * FROM class c WHERE c.id IN (SELECT class_id FROM student WHERE age > 18); -- 子查询 class_id 是普通索引(非唯一)→ type: index_subquery SET optimizer_switch = 'semijoin=on';
4.10 range:索引范围扫描
EXPLAIN SELECT * FROM student WHERE id > 3; -- type: range key: PRIMARY EXPLAIN SELECT * FROM student WHERE id BETWEEN 1 AND 3; -- range EXPLAIN SELECT * FROM student WHERE id IN (1,2,3); -- range EXPLAIN SELECT * FROM student WHERE name LIKE '张%'; -- range(前缀匹配)
<>(不等于)通常无法走 range。
4.11 index:扫描整棵索引树
EXPLAIN SELECT name FROM student; -- type: index key: idx_name Extra: Using index
只查 name 列,idx_name 索引能覆盖(不回表),但要扫完整棵索引树。
4.12 ALL:全表扫描(最差)
EXPLAIN SELECT * FROM student WHERE age = 20; -- type: ALL key: NULL rows: 5 Extra: Using where
age 只是复合索引
(name,age)的第二列,单独查 age 不符合最左前缀,且无独立索引 → 全表扫描。
五、possible_keys / key / key_len / ref
5.1 possible_keys 与 key
-- 有可用索引且被使用 EXPLAIN SELECT * FROM student WHERE name = '张三'; -- possible_keys: idx_name key: idx_name -- 有索引但优化器没用(如查询返回表中大部分数据) EXPLAIN SELECT * FROM student WHERE id > 0; -- possible_keys: PRIMARY key: NULL(数据太少/全取,优化器选全表扫描) -- 完全没有可用索引 EXPLAIN SELECT * FROM student WHERE email = 'ls@xx.com'; -- possible_keys: NULL key: NULL
5.2 key_len:判断复合索引用了几个字段
复合索引 idx_name_age(name, age),name 是 VARCHAR(20) utf8mb4 且允许 NULL,age 是 INT 允许 NULL:
-- 只用到 name:key_len = 20*4+2+1 = 83 EXPLAIN SELECT * FROM student WHERE name = '张三'; -- key_len: 83 -- 用到 name + age:83 + (4+1) = 88 EXPLAIN SELECT * FROM student WHERE name = '张三' AND age = 18; -- key_len: 88
计算规则:INT=4;CHAR(n) utf8mb4=4n;VARCHAR(n) utf8mb4=4n+2;允许 NULL 再 +1。
5.3 ref:索引与什么比较
EXPLAIN SELECT * FROM student WHERE name = '张三'; -- ref: const(与常量比较) EXPLAIN SELECT s.* FROM student s INNER JOIN class c ON s.class_id = c.id; -- student 行 ref: test.c.id(与另一表的列比较)
六、rows 与 filtered
EXPLAIN SELECT * FROM student WHERE name = '张三'; -- rows: 2 filtered: 100.00 -- 估算扫描 2 行,经过 WHERE 过滤后 100% 保留 → 预计返回 2 行 EXPLAIN SELECT * FROM student WHERE age = 20; -- type: ALL rows: 5 filtered: 20.00 -- 估算扫描 5 行,过滤后剩 20% → 预计返回约 1 行(5 × 20% = 1)
rows 是估算值不是精确值;rows 越小越好,filtered 越接近 100 越好。
七、Extra(附加信息)⭐
7.1 Using filesort:排序没走索引
EXPLAIN SELECT * FROM student WHERE class_id = 1 ORDER BY age; -- class_id 走索引,但 ORDER BY age 无法利用索引顺序 -- Extra: Using filesort
filesort 不一定用磁盘文件,数据量小时在 sort buffer 内存排序。
7.2 Using temporary:用了临时表
-- GROUP BY 非索引列 EXPLAIN SELECT age, COUNT(*) FROM student GROUP BY age; -- Extra: Using temporary; Using filesort -- DISTINCT 非索引列 EXPLAIN SELECT DISTINCT age FROM student; -- Extra: Using temporary
7.3 Using where:Server 层额外过滤
EXPLAIN SELECT * FROM student WHERE age = 20; -- type: ALL Extra: Using where(存储引擎取出后,Server 层再用 age=20 过滤)
7.4 Using index:覆盖索引,不回表
EXPLAIN SELECT name, age FROM student WHERE name = '张三'; -- 查询列 name、age 都在复合索引 idx_name_age 中 -- Extra: Using index
7.5 Using index condition:索引条件下推(ICP)
EXPLAIN SELECT * FROM student WHERE name LIKE '张%' AND age > 18; -- name 范围扫描,age>18 条件下推到存储引擎层用索引先过滤,减少回表 -- Extra: Using index condition
7.6 Using join buffer:关联缺索引
EXPLAIN SELECT * FROM student s1 INNER JOIN student s2 ON s1.age = s2.age; -- age 无索引,无法索引关联,使用 BNL 连接缓冲 -- Extra: Using join buffer (Block Nested Loop)
7.7 Impossible WHERE:条件恒假
EXPLAIN SELECT * FROM student WHERE 1 = 0; -- Extra: Impossible WHERE(rows 为空,不会执行) EXPLAIN SELECT * FROM student WHERE id = 1 AND id = 2; -- 同样 Impossible WHERE
7.8 Select tables optimized away:无需扫描即可确定结果
EXPLAIN SELECT MIN(id), MAX(id) FROM student; -- 直接从索引两端取值,优化器确定最多一行结果 -- Extra: Select tables optimized away
7.9 No tables used:无 FROM 子句
EXPLAIN SELECT 1; EXPLAIN SELECT NOW(); -- Extra: No tables used
7.10 组合问题示例(filesort + temporary 同时出现)
EXPLAIN SELECT age, COUNT(*) AS cnt FROM student WHERE id > 1 GROUP BY age ORDER BY cnt DESC; -- type: range(id 走索引) -- Extra: Using where; Using temporary; Using filesort -- 分组用临时表、排序用 filesort,是重点优化对象(可考虑给 age 加索引)
八、一页速查表
| 字段 | 关注点 | 好的表现 | 差的表现 |
|---|---|---|---|
| id | 执行顺序 | 层级清晰 | 大量 DERIVED/临时表 |
| type | 访问方式 | const/eq_ref/ref/range | index/ALL |
| key | 实际索引 | 命中预期索引 | NULL(该有索引却没有) |
| key_len | 索引利用长度 | 充分利用复合索引 | 只用到最左一个字段 |
| rows | 扫描行数 | 小 | 巨大 |
| filtered | 过滤比例 | 接近 100 | 很小(扫了大量无用行) |
| Extra | 附加信息 | Using index | Using filesort / Using temporary / Using join buffer |
总结
到此这篇关于MySQL进阶篇之SQL性能分析的文章就介绍到这了,更多相关MySQL之SQL性能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
