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

347 阅读24分钟

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

MySQL索引系列文章:

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

一、前言

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

本文内容概览:

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

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

image.png


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

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

2.1 创建表结构

-- 创建用户表
CREATE TABLE tb_user (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    name VARCHAR(50NOT NULL COMMENT '用户名',
    phone VARCHAR(11NOT NULL COMMENT '手机号',
    email VARCHAR(100) COMMENT '邮箱',
    profession VARCHAR(50) COMMENT '职业',
    age INT COMMENT '年龄',
    gender CHAR(1) COMMENT '性别:1男 2女',
    status CHAR(1DEFAULT '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_selectSELECT语句执行次数查询操作
Com_insertINSERT语句执行次数插入操作
Com_updateUPDATE语句执行次数更新操作
Com_deleteDELETE语句执行次数删除操作

示例输出:

+---------------+--------+
| 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_readSELECT查询返回的行数
Innodb_rows_insertedINSERT插入的行数
Innodb_rows_updatedUPDATE更新的行数
Innodb_rows_deletedDELETE删除的行数

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';

字段说明:

字段说明
TimeSQL执行时间
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 |  -- 清理
+----------------------+----------+

核心阶段说明:

阶段说明优化方向
optimizingSQL优化器分析如果耗时长,可能查询太复杂
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子句中的子查询)
UNIONUNION 操作的第二个或后面的查询
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,678Com_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%
索引使用
慢查询日志记录不记录
typeALLrange

八、工具对比与选择

工具作用使用场景优势劣势
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 PROFILEPerformance 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:执行计划分析

参考资料

官方文档

视频教程

分析工具

可视化工具

本文使用 markdown.com.cn 排版