MySQL索引:SQL性能分析工具详解(进阶篇)

MySQL索引:SQL性能分析工具详解(进阶篇)

Scroll Down

MySQL索引:SQL性能分析工具详解(进阶篇)

MySQL索引系列文章:

在实际开发中,如何发现慢SQL?如何分析查询瓶颈?如何验证索引是否生效?本文将深入讲解MySQL的性能分析工具,帮你成为SQL优化专家。

一、前言

索引优化不能凭感觉,而是需要科学的性能分析工具。就像医生看病需要体检报告一样,优化SQL也需要"体检报告"——这就是本文要讲的性能分析工具。

本文内容概览:

性能分析工具体系
├── SQL执行频率统计     ← 找到优化方向
├── 慢查询日志         ← 发现问题SQL
├── Profile性能分析     ← 定位耗时环节
└── EXPLAIN执行计划     ← 分析查询策略

SQL执行的核心阶段与性能瓶颈:

问题1问题2问题3问题4SQL提交优化器生成执行计划存储引擎数据读取数据过滤WHERE条件排序/分组ORDER/GROUP返回结果未使用索引EXPLAIN检查大量磁盘I/OProfile分析扫描行数多慢查询日志文件排序EXPLAIN Extra

二、准备工作:创建测试环境

在开始学习性能分析工具之前,我们先创建一张测试表并插入数据。

2.1 创建表结构

-- 创建用户表
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 插入测试数据

-- 插入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执行统计(服务器启动后的累计值)
SHOW GLOBAL STATUS LIKE 'Com_______';

注意Com_______ 是7个下划线,用于匹配以 Com_ 开头且总长度为10的状态变量

核心指标:

变量名 说明 含义
Com_select SELECT语句执行次数 查询操作
Com_insert INSERT语句执行次数 插入操作
Com_update UPDATE语句执行次数 更新操作
Com_delete DELETE语句执行次数 删除操作

示例输出:

+---------------+--------+
| Variable_name | Value  |
+---------------+--------+
| Com_select    | 156847 |
| Com_insert    | 23456  |
| Com_update    | 8765   |
| Com_delete    | 234    |
+---------------+--------+

分析结论:

Com_select:156,847 次  ← 占比最高(约82%)
Com_insert:23,456 次   ← 占比12%
Com_update:8,765 次    ← 占比5%
Com_delete:234 次      ← 占比0.1%

结论:这是一个读多写少的系统,应重点优化 SELECT 查询

3.3 查看会话级执行频率

-- 查看当前会话的SQL执行统计
SHOW SESSION STATUS LIKE 'Com_______';

GLOBAL vs SESSION 的区别:

维度 GLOBAL(全局) SESSION(会话)
统计范围 整个MySQL服务器 当前连接会话
统计起点 服务器启动时 会话创建时
应用场景 整体性能分析 单个功能测试

3.4 深入分析:查看InnoDB引擎统计

-- 查看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类型
  • 无法区分是哪张表的操作

解决方案:

如果需要表级别的统计,可以使用:

  1. 慢查询日志(下一节讲解)
  2. Performance Schema(MySQL 5.7+)
  3. General Log(性能开销大,不推荐生产环境)

四、慢查询日志

4.1 什么是慢查询日志?

慢查询日志(Slow Query Log) 是 MySQL 提供的一种日志记录功能,用于记录执行时间超过指定阈值的 SQL 语句。

类比理解: 就像工厂的"次品记录本",专门记录不合格产品,方便后续改进。

4.2 检查慢查询日志状态

-- 查看慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query_log';

-- 查看慢查询阈值(单位:秒)
SHOW VARIABLES LIKE 'long_query_time';

默认配置:

slow_query_log      : OFF     -- 默认关闭
long_query_time     : 10.0    -- 默认10秒

说明:只有执行时间超过 long_query_time 的SQL才会被记录

4.3 开启慢查询日志

方式一:临时开启(会话级别)

-- 开启慢查询日志
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
  • WindowsC:\ProgramData\MySQL\MySQL Server 8.0\my.ini

2. 在 [mysqld] 节点下添加配置

[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服务

# Linux
sudo systemctl restart mysqld

# Windows
net stop mysql80 && net start mysql80

4.4 查看慢查询日志位置

-- 查看慢查询日志文件路径
SHOW VARIABLES LIKE 'slow_query_log_file';

示例输出:

+---------------------+-----------------------------------+
| Variable_name       | Value                             |
+---------------------+-----------------------------------+
| slow_query_log_file | /var/log/mysql/mysql-slow.log     |
+---------------------+-----------------------------------+

4.5 慢查询日志内容解析

执行一条慢SQL

-- 执行一条全表扫描查询(无索引)
SELECT * FROM tb_user WHERE name = 'user500000';

查看慢查询日志

# Linux
tail -f /var/log/mysql/mysql-slow.log

# Windows
type C:\ProgramData\MySQL\MySQL Server 8.0\Data\DESKTOP-slow.log

日志内容示例:

# 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万行,说明全表扫描)

分析结论:

Rows_examined: 1,000,000  ← 全表扫描
Rows_sent: 1              ← 只返回1行
查询时间: 3.46秒

问题:扫描100万行只为找1条数据,效率极低
优化方向:在name字段创建索引

4.6 实战:优化慢查询

创建索引

-- 在name字段创建索引
CREATE INDEX idx_name ON tb_user(name);

再次执行查询

SELECT * FROM tb_user WHERE name = 'user500000';
-- 执行时间:0.003秒

查看优化后的日志

这次不会出现在慢查询日志中,因为执行时间小于2秒阈值。

性能对比:

对比项 优化前 优化后 提升倍数
查询时间 3.46秒 0.003秒 1153倍
扫描行数 1,000,000行 1行 -
是否使用索引 -

4.7 慢查询日志分析工具

对于生产环境,慢查询日志文件可能非常大,手动分析效率低。可以使用以下工具:

1. mysqldumpslow(MySQL自带)

# 按查询时间排序,显示前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(推荐)

# 安装(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功能

-- 查看当前MySQL版本是否支持Profile
SELECT @@have_profiling;

-- 查看Profile是否开启
SELECT @@profiling;

返回值说明:

  • have_profiling = YES:支持Profile功能
  • profiling = 0:未开启
  • profiling = 1:已开启

5.3 开启Profile

-- 开启当前会话的Profile(默认为SESSION级别)
SET profiling = 1;

-- 也可以明确指定作用域
SET SESSION profiling = 1;

注意:Profile 配置仅在当前会话有效,断开连接后失效

5.4 查看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;

输出示例:

+----------+------------+--------------------------------------------------------+
| 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详细执行过程

-- 查看Query_ID为3的SQL详细执行信息
SHOW PROFILE FOR QUERY 3;

输出示例:

+----------------------+----------+
| 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耗时

-- 查看CPU使用情况
SHOW PROFILE CPU FOR QUERY 3;

输出示例:

+----------------------+----------+----------+------------+
| 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时间
-- 查看I/O操作情况
SHOW PROFILE BLOCK IO FOR QUERY 3;

输出示例:

+----------------------+----------+--------------+---------------+
| Status               | Duration | Block_ops_in | Block_ops_out |
+----------------------+----------+--------------+---------------+
| Sending data         | 1.234000 |        15678 |             0 |
+----------------------+----------+--------------+---------------+

字段说明:

  • Block_ops_in:磁盘读取次数
  • Block_ops_out:磁盘写入次数

5.7 查看所有性能信息

-- 一次性查看所有性能指标
SHOW PROFILE ALL FOR QUERY 3;

包含的信息:

  • CPU使用情况
  • I/O操作次数
  • 内存使用
  • 上下文切换
  • 页错误(Page Faults)

5.8 实战案例:定位性能瓶颈

问题SQL

-- 这条SQL执行较慢
SELECT * FROM tb_user WHERE age > 30 AND status = '0';

分析步骤

1. 查看执行记录

SHOW PROFILES;
-- 发现Query_ID为3的SQL耗时1.23秒

2. 查看详细执行过程

SHOW PROFILE FOR QUERY 3;
-- 发现 Sending data 阶段耗时1.23秒,占99%

3. 查看CPU和I/O

SHOW PROFILE CPU, BLOCK IO FOR QUERY 3;
-- 发现磁盘读取15678次,说明大量磁盘I/O

4. 分析结论

瓶颈:Sending data 阶段耗时长
原因:全表扫描,大量磁盘I/O
优化方向:创建索引减少扫描行数

优化方案

-- 创建联合索引
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的局限性

注意事项:

  1. MySQL 8.0.20 开始已弃用 SHOW PROFILE

    • 官方推荐使用 Performance Schema
    • 但 Profile 仍可在低版本使用
  2. 仅能分析当前会话的SQL

    • 无法查看其他会话的执行情况
  3. 历史记录有限

    • 默认保留最近15条SQL,可通过 profiling_history_size 调整

查看和修改历史记录数量:

-- 查看保留的记录数
SHOW VARIABLES LIKE 'profiling_history_size';

-- 修改为30条
SET profiling_history_size = 30;

六、EXPLAIN执行计划

6.1 什么是执行计划?

执行计划(Execution Plan) 是 MySQL 优化器对 SQL 查询的执行策略说明,展示了:

  • SQL 如何执行
  • 是否使用索引
  • 索引是否生效
  • 表的访问顺序
  • 预计扫描的行数

类比理解: 就像导航软件规划的行车路线,告诉你会经过哪些路段,需要多长时间。

6.2 EXPLAIN 基本用法

-- 语法
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;

-- 或使用别名
DESC SELECT 字段列表 FROM 表名 WHERE 条件;

注意EXPLAIN 只是分析SQL,不会真正执行查询

示例:分析一条查询

EXPLAIN SELECT * FROM tb_user WHERE id = 100;

输出结果:

+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| 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 不同:值越大,优先级越高,越先执行

示例:多表关联查询

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;

输出:

+----+-------------+-------+------+
| 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(访问类型)⭐⭐⭐⭐⭐

重要性:这是最关键的字段,决定了查询性能!

性能从优到劣排序:

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

实战示例:

-- 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 评估后认为可能用到的索引列表

EXPLAIN SELECT * FROM tb_user WHERE age > 25 AND status = '0';

输出:

possible_keys: idx_age, idx_age_status

说明:idx_ageidx_age_status 两个索引可用,但实际使用哪个由优化器决定

5. key(实际使用的索引)⭐⭐⭐⭐⭐

含义: MySQL 最终选择使用的索引

重要性: 这是验证索引是否生效的关键字段!

EXPLAIN SELECT * FROM tb_user WHERE age > 25 AND status = '0';

输出:

key: idx_age_status  -- 使用了联合索引

判断:

  • key = NULL没有使用索引,性能很差
  • key = 索引名使用了索引,性能较好

6. key_len(索引使用的字节数)

含义: 实际使用的索引长度(字节数)

作用: 在联合索引中,可以判断使用了几个索引列

计算规则:

- INT:4字节
- BIGINT:8字节
- VARCHAR(n):n * 字符集字节数 + 2(长度标识)
- CHAR(n):n * 字符集字节数
- 允许NULL:额外 +1 字节

示例:

-- 联合索引: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 估算需要扫描的行数

重要性: 行数越少,查询越快

-- 无索引
EXPLAIN SELECT * FROM tb_user WHERE phone = '13800138000';
-- rows: 1000000(全表扫描)

-- 有索引
EXPLAIN SELECT * FROM tb_user WHERE id = 100;
-- rows: 1(精准定位)

8. filtered(过滤百分比)

含义: 满足查询条件的行数百分比

计算公式: 实际返回行数 / rows * 100%

EXPLAIN SELECT * FROM tb_user WHERE age > 30 AND status = '0';

输出:

rows: 500000
filtered: 10.00

解读:

预计扫描 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(最优)

含义: 使用了覆盖索引,无需回表查询

-- 创建索引
CREATE INDEX idx_age_name ON tb_user(age, name);

-- 覆盖索引查询
EXPLAIN SELECT age, name FROM tb_user WHERE age = 30;

输出:

Extra: Using index  -- 所有数据都在索引中,无需回表
⚠️ Using filesort(需优化)

含义: 无法使用索引排序,需要额外的文件排序(可能在磁盘)

-- 无索引的排序
EXPLAIN SELECT * FROM tb_user ORDER BY age;

输出:

Extra: Using filesort  -- 性能较差,建议优化

优化方案:

-- 在排序字段创建索引
CREATE INDEX idx_age ON tb_user(age);

-- 再次执行
EXPLAIN SELECT * FROM tb_user ORDER BY age;
-- Extra: Using index(已优化)
❌ Using temporary(最差)

含义: 使用临时表存储中间结果,性能很差

-- GROUP BY 无索引
EXPLAIN SELECT profession, COUNT(*) FROM tb_user GROUP BY profession;

输出:

Extra: Using temporary; Using filesort

优化方案:

-- 在分组字段创建索引
CREATE INDEX idx_profession ON tb_user(profession);

6.5 实战案例:EXPLAIN完整分析

案例1:主键查询(最优)

EXPLAIN SELECT * FROM tb_user WHERE id = 100;

执行计划:

+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table   | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | tb_user | const | PRIMARY       | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+

分析结论:

✅ type = const       -- 主键等值查询,性能最优
✅ key = PRIMARY      -- 使用了主键索引
✅ rows = 1           -- 只扫描1行
✅ Extra = NULL       -- 无额外操作

性能评级:⭐⭐⭐⭐⭐(满分)

案例2:范围查询(一般)

EXPLAIN SELECT * FROM tb_user WHERE age > 30;

执行计划:

+----+-------------+---------+-------+---------------+---------+---------+------+---------+-------------+
| 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 |
+----+-------------+---------+-------+---------------+---------+---------+------+---------+-------------+

分析结论:

⚠️ type = range       -- 范围查询,性能一般
✅ key = idx_age      -- 使用了索引
⚠️ rows = 500,000    -- 需要扫描50万行

性能评级:⭐⭐⭐(中等)

案例3:全表扫描(最差)

EXPLAIN SELECT * FROM tb_user WHERE phone = '13800138000';

执行计划:

+----+-------------+---------+------+---------------+------+---------+------+----------+-------------+
| 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 |
+----+-------------+---------+------+---------------+------+---------+------+----------+-------------+

分析结论:

❌ type = ALL         -- 全表扫描,性能最差
❌ key = NULL         -- 没有使用索引
❌ rows = 1,000,000   -- 扫描100万行

性能评级:⭐(需立即优化)

优化方案:

CREATE INDEX idx_phone ON tb_user(phone);

案例4:覆盖索引(最优)

-- 创建联合索引
CREATE INDEX idx_age_name_status ON tb_user(age, name, status);

-- 覆盖索引查询
EXPLAIN SELECT age, name, status FROM tb_user WHERE age = 30;

执行计划:

+----+-------------+---------+------+---------------------+---------------------+---------+-------+------+-------------+
| 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 |
+----+-------------+---------+------+---------------------+---------------------+---------+-------+------+-------------+

分析结论:

✅ type = ref              -- 非唯一索引查询,性能良好
✅ key = idx_age_name_status -- 使用了联合索引
✅ Extra = Using index     -- 覆盖索引,无需回表,性能最优

性能评级:⭐⭐⭐⭐⭐(满分)

6.6 EXPLAIN 输出格式

1. 传统格式(默认)

EXPLAIN SELECT * FROM tb_user WHERE id = 100;

2. JSON格式(更详细)

EXPLAIN FORMAT=JSON SELECT * FROM tb_user WHERE id = 100;

输出示例:

{
  "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+)

EXPLAIN FORMAT=TREE SELECT * FROM tb_user WHERE id = 100;

输出示例:

-> Rows fetched before execution  (cost=0.00 rows=1)

4. 可视化工具

推荐:IDEA DataGrip 插件

在 IDEA 中安装 DataGrip 插件后,执行计划会以图形化方式展示,更加直观。

操作步骤:

  1. 安装 DataGrip 插件
  2. 连接 MySQL 数据库
  3. 右键查询语句 → Explain PlanExplain (Visualize)

6.7 EXPLAIN 性能优化检查清单

根据 EXPLAIN 结果,按以下清单逐项检查:

✅ 必须达标

  • type 至少为 ref(避免 ALLindex
  • key 不为 NULL(确保使用了索引)
  • rows 扫描行数合理(避免大量扫描)

⭐ 优化目标

  • Extra 包含 Using index(覆盖索引)
  • Extra 不包含 Using filesort(避免文件排序)
  • Extra 不包含 Using temporary(避免临时表)

🎯 最佳实践

  • 主键查询达到 type = const
  • 范围查询达到 type = range
  • 联合索引充分利用(key_len 最大化)

七、综合实战:性能分析完整流程

假设我们收到反馈:“用户列表查询很慢”,如何系统性地分析和优化?

7.1 步骤1:统计SQL执行频率

-- 查看SQL执行频率
SHOW GLOBAL STATUS LIKE 'Com_______';

分析结果:

Com_select: 1,234,567 次  -- 查询操作占主导
Com_insert: 45,678 次
Com_update: 12,345 次

结论:这是读多写少的系统,重点优化SELECT查询

7.2 步骤2:开启慢查询日志

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 阈值1秒

-- 查看慢查询日志文件位置
SHOW VARIABLES LIKE 'slow_query_log_file';

7.3 步骤3:执行问题SQL

-- 模拟用户操作:查询30岁以上的正常用户
SELECT * FROM tb_user WHERE age > 30 AND status = '0';

7.4 步骤4:查看慢查询日志

tail -f /var/log/mysql/mysql-slow.log

日志内容:

# Query_time: 2.567  Rows_examined: 1000000
SELECT * FROM tb_user WHERE age > 30 AND status = '0';

发现: 扫描了100万行,耗时2.5秒

7.5 步骤5:使用Profile分析

-- 开启Profile
SET profiling = 1;

-- 再次执行SQL
SELECT * FROM tb_user WHERE age > 30 AND status = '0';

-- 查看Profile
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;

Profile结果:

Sending data: 2.345秒(占91%)  -- 瓶颈在数据传输

7.6 步骤6:使用EXPLAIN分析

EXPLAIN SELECT * FROM tb_user WHERE age > 30 AND status = '0';

执行计划:

type: ALL            -- 全表扫描
key: NULL            -- 没有使用索引
rows: 1,000,000      -- 扫描100万行
Extra: Using where

问题确认:

❌ 全表扫描
❌ 没有使用索引
❌ 扫描行数过多

优化方向:创建索引

7.7 步骤7:创建索引优化

-- 创建联合索引
CREATE INDEX idx_age_status ON tb_user(age, status);

-- 再次执行查询
SELECT * FROM tb_user WHERE age > 30 AND status = '0';

7.8 步骤8:验证优化效果

验证1:查看慢查询日志

# 优化后该SQL不再出现在慢查询日志中

验证2:查看Profile

SHOW PROFILES;
-- 执行时间:0.023秒(优化前2.567秒,提升111倍)

验证3:查看EXPLAIN

EXPLAIN SELECT * FROM tb_user WHERE age > 30 AND status = '0';

优化后的执行计划:

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 查看执行计划 验证索引是否生效 不实际执行,无副作用 只是预估,可能不准确

推荐使用流程:

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状态

-- 查看是否开启(MySQL 5.7+ 默认开启)
SHOW VARIABLES LIKE 'performance_schema';

9.3 使用Performance Schema分析SQL

1. 查看最耗时的SQL

-- 查询执行时间最长的前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

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统计

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_timeRows_examined
  • 使用 mysqldumpslowpt-query-digest 分析日志

3. Profile性能分析

  • 使用 SHOW PROFILE 查看SQL各阶段耗时
  • 重点关注 Sending data 阶段
  • MySQL 8.0 建议改用 Performance Schema

4. EXPLAIN执行计划

  • 最重要的性能分析工具
  • 重点关注:typekeyrowsExtra
  • 优化目标:type 至少为 ref,最好有 Using index

10.2 性能优化流程

1. 统计SQL执行频率  →  找到优化方向
2. 开启慢查询日志   →  发现慢SQL
3. 使用EXPLAIN分析  →  查看是否使用索引
4. 使用Profile分析  →  定位具体瓶颈
5. 创建索引优化     →  解决问题
6. 再次EXPLAIN验证  →  确认优化生效

10.3 EXPLAIN 性能评级标准

⭐⭐⭐⭐⭐ 优秀

type: const / eq_ref / ref
key: 使用了索引
Extra: Using index(覆盖索引)

⭐⭐⭐ 一般

type: range
key: 使用了索引
Extra: Using where

⭐ 需优化

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:执行计划分析

参考资料

官方文档

视频教程

分析工具

可视化工具