Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL之SQL性能

MySQL进阶篇之SQL性能分析示例详解

作者:bksczm

MySQL性能优化是一个不断调优的过程,从硬件配置到查询结构优化再到索引设计,都需要全面考虑,这篇文章主要介绍了MySQL进阶篇之SQL性能分析的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

准备测试表与数据

-- 班级表
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 的执行计划,而不是盲目执行后再看结果。通过分析执行计划,可以判断查询是否走了索引、扫描了多少行、是否有临时表 / 文件排序等性能问题。

是否真正执行

一、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/rangeindex/ALL
key实际索引命中预期索引NULL(该有索引却没有)
key_len索引利用长度充分利用复合索引只用到最左一个字段
rows扫描行数小巨大
filtered过滤比例接近 100很小(扫了大量无用行)
Extra附加信息Using indexUsing filesort / Using temporary / Using join buffer

总结 

到此这篇关于MySQL进阶篇之SQL性能分析的文章就介绍到这了,更多相关MySQL之SQL性能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文