MySQL索引:SQL性能分析工具详解(进阶篇)
MySQL索引系列文章:
- 📖 基础篇:图解MySQL索引:从二叉树到B+树的演进之路
- 📖 进阶篇:MySQL索引:SQL性能分析工具详解(本文)
在实际开发中,如何发现慢SQL?如何分析查询瓶颈?如何验证索引是否生效?本文将深入讲解MySQL的性能分析工具,帮你成为SQL优化专家。
一、前言
索引优化不能凭感觉,而是需要科学的性能分析工具。就像医生看病需要体检报告一样,优化SQL也需要"体检报告"------这就是本文要讲的性能分析工具。
本文内容概览:
sql
性能分析工具体系
├── SQL执行频率统计 ← 找到优化方向
├── 慢查询日志 ← 发现问题SQL
├── Profile性能分析 ← 定位耗时环节
└── EXPLAIN执行计划 ← 分析查询策略
SQL执行的核心阶段与性能瓶颈:

二、准备工作:创建测试环境
在开始学习性能分析工具之前,我们先创建一张测试表并插入数据。
2.1 创建表结构
sql
-- 创建用户表
CREATE TABLE tb_user (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
name VARCHAR(50) NOT NULL COMMENT '用户名',
phone VARCHAR(11) NOT NULL COMMENT '手机号',
email VARCHAR(100) COMMENT '邮箱',
profession VARCHAR(50) COMMENT '职业',
age INT COMMENT '年龄',
gender CHAR(1) COMMENT '性别:1男 2女',
status CHAR(1) DEFAULT '0' COMMENT '状态:0正常 1禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
2.2 插入测试数据
sql
-- 插入100万条测试数据(使用存储过程)
DELIMITER $$
CREATE PROCEDURE insert_user_data(IN num INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE batch_size INT DEFAULT 1000;
-- 关闭自动提交,提升插入速度
SET autocommit = 0;
WHILE i <= num DO
INSERT INTO tb_user(name, phone, email, profession, age, gender, status)
VALUES (
CONCAT('user', i),
CONCAT('1', LPAD(i, 10, '0')),
CONCAT('user', i, '@test.com'),
ELT(FLOOR(1 + RAND() * 5), '程序员', '教师', '医生', '销售', '律师'),
FLOOR(18 + RAND() * 50),
IF(RAND() > 0.5, '1', '2'),
IF(RAND() > 0.9, '1', '0')
);
-- 每1000条提交一次
IF i % batch_size = 0 THEN
COMMIT;
END IF;
SET i = i + 1;
END WHILE;
COMMIT;
SET autocommit = 1;
END$$
DELIMITER ;
-- 执行存储过程:插入100万条数据(执行时间约3-5分钟)
CALL insert_user_data(1000000);
-- 查看数据量
SELECT COUNT(*) FROM tb_user;
💡 说明:
DELIMITER:修改SQL语句结束符,避免存储过程中的分号被误识别LPAD(i, 10, '0'):左填充函数,生成固定长度的字符串ELT():根据索引返回字符串列表中的元素autocommit=0:关闭自动提交,批量提交提升性能
三、SQL执行频率统计
3.1 为什么要统计执行频率?
在优化数据库之前,首先要了解数据库的"工作重心":
- 如果是读多写少(如电商网站):重点优化
SELECT查询 - 如果是写多读少(如日志系统):重点优化
INSERT操作 - 如果更新频繁(如库存系统):需要考虑锁竞争问题
不是所有数据库都有性能问题,也不是所有表都需要优化。统计执行频率可以帮助我们找到优化的方向。
3.2 查看全局执行频率
sql
-- 查看全局SQL执行统计(服务器启动后的累计值)
SHOW GLOBAL STATUS LIKE 'Com_______';
注意 :
Com_______是7个下划线,用于匹配以Com_开头且总长度为10的状态变量
核心指标:
| 变量名 | 说明 | 含义 |
|---|---|---|
Com_select |
SELECT语句执行次数 | 查询操作 |
Com_insert |
INSERT语句执行次数 | 插入操作 |
Com_update |
UPDATE语句执行次数 | 更新操作 |
Com_delete |
DELETE语句执行次数 | 删除操作 |
示例输出:
diff
+---------------+--------+
| Variable_name | Value |
+---------------+--------+
| Com_select | 156847 |
| Com_insert | 23456 |
| Com_update | 8765 |
| Com_delete | 234 |
+---------------+--------+
分析结论:
erlang
Com_select:156,847 次 ← 占比最高(约82%)
Com_insert:23,456 次 ← 占比12%
Com_update:8,765 次 ← 占比5%
Com_delete:234 次 ← 占比0.1%
结论:这是一个读多写少的系统,应重点优化 SELECT 查询
3.3 查看会话级执行频率
sql
-- 查看当前会话的SQL执行统计
SHOW SESSION STATUS LIKE 'Com_______';
GLOBAL vs SESSION 的区别:
| 维度 | GLOBAL(全局) | SESSION(会话) |
|---|---|---|
| 统计范围 | 整个MySQL服务器 | 当前连接会话 |
| 统计起点 | 服务器启动时 | 会话创建时 |
| 应用场景 | 整体性能分析 | 单个功能测试 |
3.4 深入分析:查看InnoDB引擎统计
sql
-- 查看InnoDB存储引擎的读写行数
SHOW GLOBAL STATUS LIKE 'Innodb_rows_%';
重要指标:
| 变量名 | 说明 |
|---|---|
Innodb_rows_read |
SELECT查询返回的行数 |
Innodb_rows_inserted |
INSERT插入的行数 |
Innodb_rows_updated |
UPDATE更新的行数 |
Innodb_rows_deleted |
DELETE删除的行数 |
3.5 局限性
问题:统计的执行频率能否具体到表?
答案是:不能直接统计到表级别。
SHOW STATUS只能统计全局或会话级别的SQL类型- 无法区分是哪张表的操作
解决方案:
如果需要表级别的统计,可以使用:
- 慢查询日志(下一节讲解)
- Performance Schema(MySQL 5.7+)
- General Log(性能开销大,不推荐生产环境)
四、慢查询日志
4.1 什么是慢查询日志?
慢查询日志(Slow Query Log) 是 MySQL 提供的一种日志记录功能,用于记录执行时间超过指定阈值的 SQL 语句。
类比理解: 就像工厂的"次品记录本",专门记录不合格产品,方便后续改进。
4.2 检查慢查询日志状态
sql
-- 查看慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query_log';
-- 查看慢查询阈值(单位:秒)
SHOW VARIABLES LIKE 'long_query_time';
默认配置:
lua
slow_query_log : OFF -- 默认关闭
long_query_time : 10.0 -- 默认10秒
说明 :只有执行时间超过
long_query_time的SQL才会被记录
4.3 开启慢查询日志
方式一:临时开启(会话级别)
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置慢查询阈值为2秒
SET GLOBAL long_query_time = 2;
-- 验证配置
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
注意:这种方式在MySQL服务重启后失效
方式二:永久开启(修改配置文件)
1. 找到MySQL配置文件
- Linux/Mac :
/etc/my.cnf或/etc/mysql/my.cnf - Windows :
C:\ProgramData\MySQL\MySQL Server 8.0\my.ini
2. 在 [mysqld] 节点下添加配置
ini
[mysqld]
# 开启慢查询日志
slow_query_log = 1
# 慢查询阈值(单位:秒)
long_query_time = 2
# 慢查询日志文件路径
slow_query_log_file = /var/log/mysql/mysql-slow.log
# 记录没有使用索引的查询
log_queries_not_using_indexes = 1
3. 重启MySQL服务
bash
# Linux
sudo systemctl restart mysqld
# Windows
net stop mysql80 && net start mysql80
4.4 查看慢查询日志位置
sql
-- 查看慢查询日志文件路径
SHOW VARIABLES LIKE 'slow_query_log_file';
示例输出:
lua
+---------------------+-----------------------------------+
| Variable_name | Value |
+---------------------+-----------------------------------+
| slow_query_log_file | /var/log/mysql/mysql-slow.log |
+---------------------+-----------------------------------+
4.5 慢查询日志内容解析
执行一条慢SQL
sql
-- 执行一条全表扫描查询(无索引)
SELECT * FROM tb_user WHERE name = 'user500000';
查看慢查询日志
bash
# Linux
tail -f /var/log/mysql/mysql-slow.log
# Windows
type C:\ProgramData\MySQL\MySQL Server 8.0\Data\DESKTOP-slow.log
日志内容示例:
ini
# Time: 2025-11-01T10:30:45.123456Z
# User@Host: root[root] @ localhost [] Id: 8
# Query_time: 3.456789 Lock_time: 0.000123 Rows_sent: 1 Rows_examined: 1000000
SET timestamp=1730458245;
SELECT * FROM tb_user WHERE name = 'user500000';
字段说明:
| 字段 | 说明 |
|---|---|
Time |
SQL执行时间 |
User@Host |
执行用户和主机 |
Query_time |
查询总耗时(秒) ⭐ |
Lock_time |
锁等待时间(秒) |
Rows_sent |
返回的行数 |
Rows_examined |
扫描的行数 ⭐(100万行,说明全表扫描) |
分析结论:
makefile
Rows_examined: 1,000,000 ← 全表扫描
Rows_sent: 1 ← 只返回1行
查询时间: 3.46秒
问题:扫描100万行只为找1条数据,效率极低
优化方向:在name字段创建索引
4.6 实战:优化慢查询
创建索引
scss
-- 在name字段创建索引
CREATE INDEX idx_name ON tb_user(name);
再次执行查询
sql
SELECT * FROM tb_user WHERE name = 'user500000';
-- 执行时间:0.003秒
查看优化后的日志
这次不会出现在慢查询日志中,因为执行时间小于2秒阈值。
性能对比:
| 对比项 | 优化前 | 优化后 | 提升倍数 |
|---|---|---|---|
| 查询时间 | 3.46秒 | 0.003秒 | 1153倍 |
| 扫描行数 | 1,000,000行 | 1行 | - |
| 是否使用索引 | 否 | 是 | - |
4.7 慢查询日志分析工具
对于生产环境,慢查询日志文件可能非常大,手动分析效率低。可以使用以下工具:
1. mysqldumpslow(MySQL自带)
bash
# 按查询时间排序,显示前10条
mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log
# 按扫描行数排序
mysqldumpslow -s r -t 10 /var/log/mysql/mysql-slow.log
# 查看包含特定关键字的慢查询
mysqldumpslow -g 'tb_user' /var/log/mysql/mysql-slow.log
参数说明:
-s:排序方式(t=时间,r=扫描行数,c=查询次数)-t:显示前N条-g:grep过滤,支持正则表达式
2. pt-query-digest(推荐)
bash
# 安装(Linux)
wget percona.com/get/pt-query-digest
chmod +x pt-query-digest
# 分析慢查询日志
pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txt
优势:
- 自动分类统计相似SQL
- 生成详细分析报告
- 支持导出HTML格式
五、Profile性能分析
5.1 为什么需要Profile?
场景: 有些SQL虽然不是慢查询(未超过阈值),但相对其他查询仍然较慢,如何定位瓶颈?
慢查询日志的局限性:
- 只能捕获超过阈值的SQL
- 无法看到SQL内部各阶段的耗时细节
Profile 的作用:
可以详细查看SQL执行过程中各个阶段的耗时,精确到毫秒级,帮助定位性能瓶颈。
5.2 检查Profile功能
sql
-- 查看当前MySQL版本是否支持Profile
SELECT @@have_profiling;
-- 查看Profile是否开启
SELECT @@profiling;
返回值说明:
have_profiling = YES:支持Profile功能profiling = 0:未开启profiling = 1:已开启
5.3 开启Profile
ini
-- 开启当前会话的Profile(默认为SESSION级别)
SET profiling = 1;
-- 也可以明确指定作用域
SET SESSION profiling = 1;
注意:Profile 配置仅在当前会话有效,断开连接后失效
5.4 查看SQL执行记录
sql
-- 执行几条测试SQL
SELECT * FROM tb_user WHERE id = 100;
SELECT * FROM tb_user WHERE name = 'user500000';
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
-- 查看最近执行的SQL列表
SHOW PROFILES;
输出示例:
sql
+----------+------------+--------------------------------------------------------+
| Query_ID | Duration | Query |
+----------+------------+--------------------------------------------------------+
| 1 | 0.00023400 | SELECT * FROM tb_user WHERE id = 100 |
| 2 | 0.00345600 | SELECT * FROM tb_user WHERE name = 'user500000' |
| 3 | 1.23456700 | SELECT * FROM tb_user WHERE age > 30 AND status = '0' |
+----------+------------+--------------------------------------------------------+
字段说明:
Query_ID:SQL语句的唯一标识Duration:执行总耗时(秒)Query:SQL语句内容
5.5 查看SQL详细执行过程
sql
-- 查看Query_ID为3的SQL详细执行信息
SHOW PROFILE FOR QUERY 3;
输出示例:
sql
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000087 | -- 查询开始
| checking permissions | 0.000012 | -- 检查权限
| Opening tables | 0.000034 | -- 打开表
| init | 0.000045 | -- 初始化
| System lock | 0.000019 | -- 系统锁
| optimizing | 0.000023 | -- 优化器分析
| statistics | 0.000056 | -- 统计信息
| preparing | 0.000034 | -- 准备执行
| executing | 0.000012 | -- 开始执行
| Sending data | 1.234000 | -- 传输数据(耗时最多) ⭐
| end | 0.000023 | -- 查询结束
| query end | 0.000019 | -- 查询完成
| closing tables | 0.000015 | -- 关闭表
| freeing items | 0.000034 | -- 释放资源
| cleaning up | 0.000023 | -- 清理
+----------------------+----------+
核心阶段说明:
| 阶段 | 说明 | 优化方向 |
|---|---|---|
optimizing |
SQL优化器分析 | 如果耗时长,可能查询太复杂 |
statistics |
统计信息收集 | 如果耗时长,执行 ANALYZE TABLE |
preparing |
准备执行计划 | - |
executing |
执行查询 | - |
Sending data |
数据传输 | 通常是主要瓶颈 ⭐ |
重要 :
Sending data不仅仅是网络传输,还包括磁盘读取、数据过滤等操作
5.6 查看CPU和I/O耗时
sql
-- 查看CPU使用情况
SHOW PROFILE CPU FOR QUERY 3;
输出示例:
lua
+----------------------+----------+----------+------------+
| Status | Duration | CPU_user | CPU_system |
+----------------------+----------+----------+------------+
| starting | 0.000087 | 0.000000 | 0.000000 |
| Sending data | 1.234000 | 0.456000 | 0.123000 | ⭐
| end | 0.000023 | 0.000000 | 0.000000 |
+----------------------+----------+----------+------------+
字段说明:
CPU_user:用户态CPU时间CPU_system:内核态CPU时间
sql
-- 查看I/O操作情况
SHOW PROFILE BLOCK IO FOR QUERY 3;
输出示例:
diff
+----------------------+----------+--------------+---------------+
| Status | Duration | Block_ops_in | Block_ops_out |
+----------------------+----------+--------------+---------------+
| Sending data | 1.234000 | 15678 | 0 |
+----------------------+----------+--------------+---------------+
字段说明:
Block_ops_in:磁盘读取次数Block_ops_out:磁盘写入次数
5.7 查看所有性能信息
sql
-- 一次性查看所有性能指标
SHOW PROFILE ALL FOR QUERY 3;
包含的信息:
- CPU使用情况
- I/O操作次数
- 内存使用
- 上下文切换
- 页错误(Page Faults)
5.8 实战案例:定位性能瓶颈
问题SQL
sql
-- 这条SQL执行较慢
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
分析步骤
1. 查看执行记录
sql
SHOW PROFILES;
-- 发现Query_ID为3的SQL耗时1.23秒
2. 查看详细执行过程
sql
SHOW PROFILE FOR QUERY 3;
-- 发现 Sending data 阶段耗时1.23秒,占99%
3. 查看CPU和I/O
sql
SHOW PROFILE CPU, BLOCK IO FOR QUERY 3;
-- 发现磁盘读取15678次,说明大量磁盘I/O
4. 分析结论
css
瓶颈:Sending data 阶段耗时长
原因:全表扫描,大量磁盘I/O
优化方向:创建索引减少扫描行数
优化方案
sql
-- 创建联合索引
CREATE INDEX idx_age_status ON tb_user(age, status);
-- 再次执行
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
-- 查看Profile
SHOW PROFILES;
-- 优化后耗时:0.025秒,性能提升49倍
5.9 Profile的局限性
注意事项:
-
MySQL 8.0.20 开始已弃用 SHOW PROFILE
- 官方推荐使用 Performance Schema
- 但 Profile 仍可在低版本使用
-
仅能分析当前会话的SQL
- 无法查看其他会话的执行情况
-
历史记录有限
- 默认保留最近15条SQL,可通过
profiling_history_size调整
- 默认保留最近15条SQL,可通过
查看和修改历史记录数量:
sql
-- 查看保留的记录数
SHOW VARIABLES LIKE 'profiling_history_size';
-- 修改为30条
SET profiling_history_size = 30;
六、EXPLAIN执行计划
6.1 什么是执行计划?
执行计划(Execution Plan) 是 MySQL 优化器对 SQL 查询的执行策略说明,展示了:
- SQL 如何执行
- 是否使用索引
- 索引是否生效
- 表的访问顺序
- 预计扫描的行数
类比理解: 就像导航软件规划的行车路线,告诉你会经过哪些路段,需要多长时间。
6.2 EXPLAIN 基本用法
sql
-- 语法
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;
-- 或使用别名
DESC SELECT 字段列表 FROM 表名 WHERE 条件;
注意 :
EXPLAIN只是分析SQL,不会真正执行查询
示例:分析一条查询
ini
EXPLAIN SELECT * FROM tb_user WHERE id = 100;
输出结果:
sql
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | tb_user | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
6.3 执行计划字段详解
EXPLAIN 输出包含12个字段,我们重点讲解最重要的几个:
🔴 重点字段(必须掌握)
| 字段 | 说明 | 重要程度 |
|---|---|---|
| type | 访问类型 | ⭐⭐⭐⭐⭐ |
| possible_keys | 可能使用的索引 | ⭐⭐⭐⭐ |
| key | 实际使用的索引 | ⭐⭐⭐⭐⭐ |
| rows | 预计扫描行数 | ⭐⭐⭐⭐ |
| filtered | 过滤百分比 | ⭐⭐⭐ |
| Extra | 额外信息 | ⭐⭐⭐⭐ |
🟡 次要字段(了解即可)
| 字段 | 说明 |
|---|---|
id |
查询序列号 |
select_type |
查询类型 |
table |
表名 |
partitions |
匹配的分区 |
key_len |
索引使用的字节数 |
ref |
与索引比较的列 |
6.4 字段详解
1. id(查询序列号)
含义: SQL 执行的顺序标识
规则:
id相同:从上往下顺序执行id不同:值越大,优先级越高,越先执行
示例:多表关联查询
sql
EXPLAIN
SELECT u.*, o.order_no
FROM tb_user u
LEFT JOIN tb_order o ON u.id = o.user_id
WHERE u.age > 25;
输出:
sql
+----+-------------+-------+------+
| id | select_type | table | type |
+----+-------------+-------+------+
| 1 | SIMPLE | u | ALL | -- id=1,第一个执行
| 1 | SIMPLE | o | ref | -- id=1,第二个执行
+----+-------------+-------+------+
2. select_type(查询类型)
常见取值:
| 值 | 说明 |
|---|---|
SIMPLE |
简单查询(不包含子查询或UNION) |
PRIMARY |
主查询(外层查询) |
SUBQUERY |
子查询 |
DERIVED |
派生表(FROM子句中的子查询) |
UNION |
UNION 操作的第二个或后面的查询 |
3. type(访问类型)⭐⭐⭐⭐⭐
重要性:这是最关键的字段,决定了查询性能!
性能从优到劣排序:
sql
NULL > system > const > eq_ref > ref > range > index > ALL
详细说明:
| 类型 | 说明 | 示例 | 性能 |
|---|---|---|---|
| NULL | 不访问表或索引 | SELECT 1 |
最优 ⭐⭐⭐⭐⭐ |
| system | 表只有一行(系统表) | - | 最优 ⭐⭐⭐⭐⭐ |
| const | 主键或唯一索引等值查询 | WHERE id = 1 |
优秀 ⭐⭐⭐⭐⭐ |
| eq_ref | 唯一索引扫描(JOIN) | - | 优秀 ⭐⭐⭐⭐ |
| ref | 非唯一索引等值查询 | WHERE name = 'Tom' |
良好 ⭐⭐⭐⭐ |
| range | 范围扫描 | WHERE age > 20 |
一般 ⭐⭐⭐ |
| index | 索引全扫描 | SELECT id FROM user |
较差 ⭐⭐ |
| ALL | 全表扫描 | WHERE phone = '...'(无索引) |
最差 ⭐ |
优化目标: 至少达到 ref 级别,最好是 const
实战示例:
ini
-- type = const(最优)
EXPLAIN SELECT * FROM tb_user WHERE id = 100;
-- type = ref(良好)
EXPLAIN SELECT * FROM tb_user WHERE name = 'user500000';
-- type = range(一般)
EXPLAIN SELECT * FROM tb_user WHERE age > 30;
-- type = ALL(最差,需优化)
EXPLAIN SELECT * FROM tb_user WHERE phone = '13800138000';
4. possible_keys(可能使用的索引)
含义: MySQL 评估后认为可能用到的索引列表
ini
EXPLAIN SELECT * FROM tb_user WHERE age > 25 AND status = '0';
输出:
makefile
possible_keys: idx_age, idx_age_status
说明: 有 idx_age 和 idx_age_status 两个索引可用,但实际使用哪个由优化器决定
5. key(实际使用的索引)⭐⭐⭐⭐⭐
含义: MySQL 最终选择使用的索引
重要性: 这是验证索引是否生效的关键字段!
ini
EXPLAIN SELECT * FROM tb_user WHERE age > 25 AND status = '0';
输出:
lua
key: idx_age_status -- 使用了联合索引
判断:
key = NULL:没有使用索引,性能很差 ❌key = 索引名:使用了索引,性能较好 ✅
6. key_len(索引使用的字节数)
含义: 实际使用的索引长度(字节数)
作用: 在联合索引中,可以判断使用了几个索引列
计算规则:
diff
- INT:4字节
- BIGINT:8字节
- VARCHAR(n):n * 字符集字节数 + 2(长度标识)
- CHAR(n):n * 字符集字节数
- 允许NULL:额外 +1 字节
示例:
sql
-- 联合索引:idx_age_status(age, status)
-- age: INT(4字节) + NULL(1字节) = 5
-- status: CHAR(1) * 4(utf8mb4) + NULL(1字节) = 5
-- 总计:10字节
EXPLAIN SELECT * FROM tb_user WHERE age = 30;
-- key_len = 5 ← 只使用了age列
EXPLAIN SELECT * FROM tb_user WHERE age = 30 AND status = '0';
-- key_len = 10 ← 使用了age和status两列
7. rows(预计扫描行数)⭐⭐⭐⭐
含义: MySQL 估算需要扫描的行数
重要性: 行数越少,查询越快
sql
-- 无索引
EXPLAIN SELECT * FROM tb_user WHERE phone = '13800138000';
-- rows: 1000000(全表扫描)
-- 有索引
EXPLAIN SELECT * FROM tb_user WHERE id = 100;
-- rows: 1(精准定位)
8. filtered(过滤百分比)
含义: 满足查询条件的行数百分比
计算公式: 实际返回行数 / rows * 100%
ini
EXPLAIN SELECT * FROM tb_user WHERE age > 30 AND status = '0';
输出:
makefile
rows: 500000
filtered: 10.00
解读:
ini
预计扫描 500,000 行
其中 10% 满足条件(status='0')
最终返回约 50,000 行
9. Extra(额外信息)⭐⭐⭐⭐
含义: 额外的执行信息,非常重要!
常见取值:
| 值 | 说明 | 性能 |
|---|---|---|
| Using index | 覆盖索引(无需回表) | 优秀 ✅ |
| Using where | 使用WHERE过滤 | 正常 |
| Using index condition | 索引条件下推(ICP) | 良好 ✅ |
| Using filesort | 文件排序(磁盘排序) | 较差 ⚠️ |
| Using temporary | 使用临时表 | 差 ❌ |
| Using join buffer | 使用JOIN缓冲区 | 一般 |
详细说明:
✅ Using index(最优)
含义: 使用了覆盖索引,无需回表查询
sql
-- 创建索引
CREATE INDEX idx_age_name ON tb_user(age, name);
-- 覆盖索引查询
EXPLAIN SELECT age, name FROM tb_user WHERE age = 30;
输出:
sql
Extra: Using index -- 所有数据都在索引中,无需回表
⚠️ Using filesort(需优化)
含义: 无法使用索引排序,需要额外的文件排序(可能在磁盘)
sql
-- 无索引的排序
EXPLAIN SELECT * FROM tb_user ORDER BY age;
输出:
sql
Extra: Using filesort -- 性能较差,建议优化
优化方案:
sql
-- 在排序字段创建索引
CREATE INDEX idx_age ON tb_user(age);
-- 再次执行
EXPLAIN SELECT * FROM tb_user ORDER BY age;
-- Extra: Using index(已优化)
❌ Using temporary(最差)
含义: 使用临时表存储中间结果,性能很差
vbnet
-- GROUP BY 无索引
EXPLAIN SELECT profession, COUNT(*) FROM tb_user GROUP BY profession;
输出:
vbnet
Extra: Using temporary; Using filesort
优化方案:
scss
-- 在分组字段创建索引
CREATE INDEX idx_profession ON tb_user(profession);
6.5 实战案例:EXPLAIN完整分析
案例1:主键查询(最优)
ini
EXPLAIN SELECT * FROM tb_user WHERE id = 100;
执行计划:
sql
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | tb_user | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
分析结论:
ini
✅ type = const -- 主键等值查询,性能最优
✅ key = PRIMARY -- 使用了主键索引
✅ rows = 1 -- 只扫描1行
✅ Extra = NULL -- 无额外操作
性能评级:⭐⭐⭐⭐⭐(满分)
案例2:范围查询(一般)
sql
EXPLAIN SELECT * FROM tb_user WHERE age > 30;
执行计划:
sql
+----+-------------+---------+-------+---------------+---------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+---------------+---------+---------+------+---------+-------------+
| 1 | SIMPLE | tb_user | range | idx_age | idx_age | 5 | NULL | 500000 | Using where |
+----+-------------+---------+-------+---------------+---------+---------+------+---------+-------------+
分析结论:
ini
⚠️ type = range -- 范围查询,性能一般
✅ key = idx_age -- 使用了索引
⚠️ rows = 500,000 -- 需要扫描50万行
性能评级:⭐⭐⭐(中等)
案例3:全表扫描(最差)
ini
EXPLAIN SELECT * FROM tb_user WHERE phone = '13800138000';
执行计划:
sql
+----+-------------+---------+------+---------------+------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+------+---------+------+----------+-------------+
| 1 | SIMPLE | tb_user | ALL | NULL | NULL | NULL | NULL | 1000000 | Using where |
+----+-------------+---------+------+---------------+------+---------+------+----------+-------------+
分析结论:
ini
❌ type = ALL -- 全表扫描,性能最差
❌ key = NULL -- 没有使用索引
❌ rows = 1,000,000 -- 扫描100万行
性能评级:⭐(需立即优化)
优化方案:
scss
CREATE INDEX idx_phone ON tb_user(phone);
案例4:覆盖索引(最优)
sql
-- 创建联合索引
CREATE INDEX idx_age_name_status ON tb_user(age, name, status);
-- 覆盖索引查询
EXPLAIN SELECT age, name, status FROM tb_user WHERE age = 30;
执行计划:
sql
+----+-------------+---------+------+---------------------+---------------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------------+---------------------+---------+-------+------+-------------+
| 1 | SIMPLE | tb_user | ref | idx_age_name_status | idx_age_name_status | 5 | const | 1000 | Using index |
+----+-------------+---------+------+---------------------+---------------------+---------+-------+------+-------------+
分析结论:
ini
✅ type = ref -- 非唯一索引查询,性能良好
✅ key = idx_age_name_status -- 使用了联合索引
✅ Extra = Using index -- 覆盖索引,无需回表,性能最优
性能评级:⭐⭐⭐⭐⭐(满分)
6.6 EXPLAIN 输出格式
1. 传统格式(默认)
ini
EXPLAIN SELECT * FROM tb_user WHERE id = 100;
2. JSON格式(更详细)
ini
EXPLAIN FORMAT=JSON SELECT * FROM tb_user WHERE id = 100;
输出示例:
css
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "1.00"
--
查询成本
},
"table": {
"table_name": "tb_user",
"access_type": "const",
"key": "PRIMARY",
"used_key_parts": [
"id"
],
"rows_examined_per_scan": 1,
"filtered": "100.00"
}
}
}
优势: 包含更多细节,如查询成本(cost)
3. TREE格式(MySQL 8.0.16+)
ini
EXPLAIN FORMAT=TREE SELECT * FROM tb_user WHERE id = 100;
输出示例:
ini
-> Rows fetched before execution (cost=0.00 rows=1)
4. 可视化工具
推荐:IDEA DataGrip 插件
在 IDEA 中安装 DataGrip 插件后,执行计划会以图形化方式展示,更加直观。
操作步骤:
- 安装 DataGrip 插件
- 连接 MySQL 数据库
- 右键查询语句 →
Explain Plan→Explain (Visualize)
6.7 EXPLAIN 性能优化检查清单
根据 EXPLAIN 结果,按以下清单逐项检查:
✅ 必须达标
- type 至少为
ref(避免ALL和index) - key 不为
NULL(确保使用了索引) - rows 扫描行数合理(避免大量扫描)
⭐ 优化目标
- Extra 包含
Using index(覆盖索引) - Extra 不包含
Using filesort(避免文件排序) - Extra 不包含
Using temporary(避免临时表)
🎯 最佳实践
- 主键查询达到 type = const
- 范围查询达到 type = range
- 联合索引充分利用(
key_len最大化)
七、综合实战:性能分析完整流程
假设我们收到反馈:"用户列表查询很慢",如何系统性地分析和优化?
7.1 步骤1:统计SQL执行频率
sql
-- 查看SQL执行频率
SHOW GLOBAL STATUS LIKE 'Com_______';
分析结果:
vbnet
Com_select: 1,234,567 次 -- 查询操作占主导
Com_insert: 45,678 次
Com_update: 12,345 次
结论:这是读多写少的系统,重点优化SELECT查询
7.2 步骤2:开启慢查询日志
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 阈值1秒
-- 查看慢查询日志文件位置
SHOW VARIABLES LIKE 'slow_query_log_file';
7.3 步骤3:执行问题SQL
sql
-- 模拟用户操作:查询30岁以上的正常用户
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
7.4 步骤4:查看慢查询日志
bash
tail -f /var/log/mysql/mysql-slow.log
日志内容:
ini
# Query_time: 2.567 Rows_examined: 1000000
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
发现: 扫描了100万行,耗时2.5秒
7.5 步骤5:使用Profile分析
sql
-- 开启Profile
SET profiling = 1;
-- 再次执行SQL
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
-- 查看Profile
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
Profile结果:
kotlin
Sending data: 2.345秒(占91%) -- 瓶颈在数据传输
7.6 步骤6:使用EXPLAIN分析
ini
EXPLAIN SELECT * FROM tb_user WHERE age > 30 AND status = '0';
执行计划:
sql
type: ALL -- 全表扫描
key: NULL -- 没有使用索引
rows: 1,000,000 -- 扫描100万行
Extra: Using where
问题确认:
❌ 全表扫描
❌ 没有使用索引
❌ 扫描行数过多
优化方向:创建索引
7.7 步骤7:创建索引优化
sql
-- 创建联合索引
CREATE INDEX idx_age_status ON tb_user(age, status);
-- 再次执行查询
SELECT * FROM tb_user WHERE age > 30 AND status = '0';
7.8 步骤8:验证优化效果
验证1:查看慢查询日志
bash
# 优化后该SQL不再出现在慢查询日志中
验证2:查看Profile
sql
SHOW PROFILES;
-- 执行时间:0.023秒(优化前2.567秒,提升111倍)
验证3:查看EXPLAIN
ini
EXPLAIN SELECT * FROM tb_user WHERE age > 30 AND status = '0';
优化后的执行计划:
sql
type: range -- 范围查询(优化✅)
key: idx_age_status -- 使用了索引(优化✅)
rows: 50,000 -- 扫描5万行(优化✅)
Extra: Using index condition -- 索引条件下推(优化✅)
7.9 性能对比总结
| 对比项 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 执行时间 | 2.567秒 | 0.023秒 | 111倍 |
| 扫描行数 | 1,000,000行 | 50,000行 | 减少95% |
| 索引使用 | 否 | 是 | ✅ |
| 慢查询日志 | 记录 | 不记录 | ✅ |
| type | ALL | range | ✅ |
八、工具对比与选择
| 工具 | 作用 | 使用场景 | 优势 | 劣势 |
|---|---|---|---|---|
| SQL执行频率 | 统计SQL类型分布 | 确定优化方向 | 宏观分析 | 无法定位具体SQL |
| 慢查询日志 | 记录慢SQL | 发现性能问题 | 自动记录,持久化 | 需配置阈值 |
| Profile | 分析SQL各阶段耗时 | 定位性能瓶颈 | 详细的阶段分析 | MySQL 8.0已弃用 |
| EXPLAIN | 查看执行计划 | 验证索引是否生效 | 不实际执行,无副作用 | 只是预估,可能不准确 |
推荐使用流程:
markdown
1. SQL执行频率统计 → 确定优化方向(优化SELECT还是INSERT)
2. 慢查询日志 → 找到具体的慢SQL
3. EXPLAIN → 分析为什么慢(是否使用索引)
4. Profile → 定位慢在哪个环节
5. 创建索引优化 → 解决问题
6. EXPLAIN验证 → 确认优化生效
九、MySQL 8.0 新特性:Performance Schema
9.1 为什么需要Performance Schema?
Profile 的局限性:
- MySQL 8.0.20 开始已弃用
SHOW PROFILE - 无法跨会话分析
- 功能有限
Performance Schema 的优势:
- ✅ 更强大的性能监控体系
- ✅ 支持全局性能分析
- ✅ 可以追踪锁等待、I/O操作等
- ✅ MySQL 官方推荐的性能分析工具
9.2 检查Performance Schema状态
sql
-- 查看是否开启(MySQL 5.7+ 默认开启)
SHOW VARIABLES LIKE 'performance_schema';
9.3 使用Performance Schema分析SQL
1. 查看最耗时的SQL
vbnet
-- 查询执行时间最长的前10条SQL
SELECT
DIGEST_TEXT AS '查询语句',
COUNT_STAR AS '执行次数',
AVG_TIMER_WAIT / 1000000000 AS '平均执行时间(秒)',
SUM_ROWS_EXAMINED AS '总扫描行数'
FROM
performance_schema.events_statements_summary_by_digest
ORDER BY
AVG_TIMER_WAIT DESC
LIMIT 10;
2. 查看当前正在执行的SQL
vbnet
SELECT
THREAD_ID AS '线程ID',
EVENT_NAME AS '事件类型',
SQL_TEXT AS 'SQL语句',
TIMER_WAIT / 1000000000 AS '执行时间(秒)'
FROM
performance_schema.events_statements_current
WHERE
SQL_TEXT IS NOT NULL
ORDER BY
TIMER_WAIT DESC;
3. 查看表的I/O统计
vbnet
SELECT
OBJECT_SCHEMA AS '数据库',
OBJECT_NAME AS '表名',
COUNT_READ AS '读取次数',
COUNT_WRITE AS '写入次数',
SUM_TIMER_WAIT / 1000000000 AS '总耗时(秒)'
FROM
performance_schema.table_io_waits_summary_by_table
WHERE
OBJECT_SCHEMA = 'your_database_name'
ORDER BY
SUM_TIMER_WAIT DESC
LIMIT 10;
9.4 Performance Schema vs Profile
| 对比项 | SHOW PROFILE | Performance Schema |
|---|---|---|
| 推荐程度 | MySQL 8.0已弃用 | 官方推荐 ⭐ |
| 分析范围 | 当前会话 | 全局 |
| 功能丰富度 | 基础 | 强大 |
| 学习成本 | 低 | 中 |
十、总结
本文系统讲解了 MySQL 的四大性能分析工具:
10.1 核心要点
1. SQL执行频率统计
- 使用
SHOW GLOBAL STATUS LIKE 'Com_______'查看SQL类型分布 - 确定是读多写少还是写多读少,明确优化方向
2. 慢查询日志
- 通过
slow_query_log自动记录慢SQL - 重点关注
Query_time和Rows_examined - 使用
mysqldumpslow或pt-query-digest分析日志
3. Profile性能分析
- 使用
SHOW PROFILE查看SQL各阶段耗时 - 重点关注
Sending data阶段 - MySQL 8.0 建议改用 Performance Schema
4. EXPLAIN执行计划
- 最重要的性能分析工具
- 重点关注:
type、key、rows、Extra - 优化目标:
type至少为ref,最好有Using index
10.2 性能优化流程
markdown
1. 统计SQL执行频率 → 找到优化方向
2. 开启慢查询日志 → 发现慢SQL
3. 使用EXPLAIN分析 → 查看是否使用索引
4. 使用Profile分析 → 定位具体瓶颈
5. 创建索引优化 → 解决问题
6. 再次EXPLAIN验证 → 确认优化生效
10.3 EXPLAIN 性能评级标准
⭐⭐⭐⭐⭐ 优秀
vbnet
type: const / eq_ref / ref
key: 使用了索引
Extra: Using index(覆盖索引)
⭐⭐⭐ 一般
vbnet
type: range
key: 使用了索引
Extra: Using where
⭐ 需优化
vbnet
type: ALL
key: NULL
Extra: Using filesort / Using temporary
10.4 术语表
- SQL:Structured Query Language,结构化查询语言
- I/O:Input/Output,输入输出操作
- ICP:Index Condition Pushdown,索引条件下推
- BST:Binary Search Tree,二叉搜索树
- Profile:性能剖析
- EXPLAIN:执行计划分析
参考资料
官方文档
- MySQL 8.0 Reference Manual - EXPLAIN
- MySQL 8.0 Reference Manual - Slow Query Log
- MySQL 8.0 Performance Schema
视频教程
分析工具
可视化工具
本文使用 markdown.com.cn 排版