执行计划与慢查询定位
数据库线上性能问题最常见的表象是:
- 接口 RT(响应时间)变高
- 某些 SQL 特别慢
- CPU 或 IO 异常抖动
- 数据库连接池爆满
这类问题如果只靠肉眼看 SQL 文本,很难判断真正瓶颈。执行计划和慢查询定位,就是把"SQL 看起来没问题"转成"数据库实际怎么执行"的关键手段。
一、执行计划详解
1.1 什么是执行计划
执行计划(Execution Plan)是数据库查询优化器根据 SQL 语句生成的执行方案,描述了数据库如何检索数据、使用哪些索引、表的连接顺序、扫描方式等关键信息。通过执行计划可以:
- 判断 SQL 是否使用了索引
- 了解数据扫描的行数和方式
- 发现性能瓶颈(全表扫描、临时表、文件排序等)
- 评估 SQL 的执行成本
1.2 EXPLAIN 基本用法
基本语法
-- 查看执行计划(不实际执行)
EXPLAIN SELECT * FROM users WHERE age > 30;
-- MySQL 8.0+ 支持更详细的格式
EXPLAIN FORMAT=TREE SELECT * FROM users WHERE age > 30;
-- 实际执行并返回详细统计信息(MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 30;EXPLAIN vs EXPLAIN ANALYZE
| 特性 | EXPLAIN | EXPLAIN ANALYZE |
|---|---|---|
| 是否执行SQL | 否(仅估算) | 是(实际执行) |
| 输出内容 | 预估的执行计划 | 预估 + 实际执行统计 |
| 准确性 | 基于统计信息估算 | 包含真实执行数据 |
| 适用场景 | 日常分析、生产环境 | 测试环境、性能调优 |
| 性能影响 | 无 | 可能较慢(实际执行) |
重要提示:
EXPLAIN ANALYZE会实际执行 SQL,生产环境慎用(特别是慢查询)EXPLAIN仅生成执行计划,不会执行 SQL,适合生产环境
1.3 EXPLAIN 输出字段详解
完整示例
EXPLAIN SELECT o.order_id, o.order_no, u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.status = 'PAID' AND u.age > 25
ORDER BY o.created_at DESC
LIMIT 10;输出结果:
+----+-------------+-------+------------+--------+---------------------+---------------------+---------+----------------------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------------+---------------------+---------+----------------------+------+----------+---------------------------------------+
| 1 | SIMPLE | o | NULL | ref | idx_user_status,idx_status | idx_status | 10 | const | 1520 | 10.00 | Using index condition; Using filesort |
| 1 | SIMPLE | u | NULL | eq_ref | PRIMARY,idx_age | PRIMARY | 4 | test.o.user_id | 1 | 33.33 | Using where |
+----+-------------+-------+------------+--------+---------------------+---------------------+---------+----------------------+------+----------+---------------------------------------+id - 查询标识符
含义:标识 SELECT 的序号,表示查询的执行顺序
规则:
- id 相同:从上往下顺序执行
- id 不同:id 越大越先执行(子查询)
- id 为 NULL:表示结果集,用于合并结果
示例:
-- 关联查询:id 相同,从上往下执行
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- 结果:id=1(table=orders), id=1(table=users) → 先查 orders,再查 users
-- 子查询:id 不同,大的先执行
EXPLAIN SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE age > 30);
-- 结果:id=2(子查询,users表), id=1(主查询,orders表) → 先执行子查询select_type - 查询类型
含义:表示查询的类型,主要用于区分简单查询、复杂查询
| 类型 | 说明 | 性能影响 |
|---|---|---|
| SIMPLE | 简单查询,不包含子查询或 UNION | √ 好 |
| PRIMARY | 外层查询(复杂查询的主查询) | 正常 |
| SUBQUERY | 子查询(SELECT/WHERE 中的子查询) | 可能慢 |
| DERIVED | 派生表(FROM 子句中的子查询) | 较慢,会创建临时表 |
| UNION | UNION 中的第二个或后面的查询 | 需要合并结果 |
| UNION RESULT | UNION 的结果集 | 需要创建临时表 |
| DEPENDENT SUBQUERY | 依赖外层查询的子查询 | × 慢,会执行多次 |
| DEPENDENT UNION | 依赖外层查询的 UNION | × 慢 |
| MATERIALIZED | 物化子查询(MySQL 5.6+) | 中等 |
示例:
-- SIMPLE:简单查询
EXPLAIN SELECT * FROM users WHERE age > 30;
-- SUBQUERY:子查询
EXPLAIN SELECT * FROM orders WHERE user_id = (SELECT id FROM users WHERE username = 'admin');
-- DERIVED:派生表
EXPLAIN SELECT * FROM (SELECT user_id, COUNT(*) as cnt FROM orders GROUP BY user_id) t WHERE cnt > 10;
-- DEPENDENT SUBQUERY:依赖外层的子查询(性能差)
EXPLAIN SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);优化建议:
- 尽量避免
DEPENDENT SUBQUERY,可改写为 JOIN DERIVED会创建临时表,数据量大时考虑改写
table - 访问的表
含义:显示这一行数据是关于哪张表的
特殊值:
<derivedN>:派生表,N 为 id 值<unionM,N>:UNION 结果,M 和 N 为 id 值<subqueryN>:物化子查询
partitions - 分区信息
含义:匹配的分区,非分区表为 NULL
示例:
-- 分区表演示
CREATE TABLE order_partition (
order_id INT,
created_at DATE
) PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);
EXPLAIN SELECT * FROM order_partition WHERE created_at >= '2023-01-01';
-- partitions: p2023,p2024type - 访问类型(重要)
含义:表示 MySQL 如何查找数据,从好到差依次为:
| 类型 | 说明 | 性能 | 示例场景 |
|---|---|---|---|
| system | 表只有一行(system 表) | 系统表 | |
| const | 单行匹配,主键或唯一索引 | WHERE id = 100 | |
| eq_ref | 关联查询,使用主键或唯一索引 | JOIN ... ON a.id = b.id | |
| ref | 非唯一索引,返回多行 | WHERE idx_col = 'value' | |
| fulltext | 全文索引 | 全文搜索 | |
| ref_or_null | 类似 ref,但包含 NULL | WHERE idx_col = 'value' OR idx_col IS NULL | |
| index_merge | 索引合并 | 多个索引条件 OR 连接 | |
| range | 索引范围扫描 | WHERE age > 20 AND age < 30 | |
| index | 全索引扫描 | 扫描整个索引树 | |
| ALL | 全表扫描 | 无索引或索引失效 |
详细说明:
1. system (最优)
-- MyISAM 或 Memory 引擎的系统表
EXPLAIN SELECT * FROM mysql.proxies_priv WHERE 1=1;2. const (最优)
-- 主键或唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE id = 100; -- 主键
EXPLAIN SELECT * FROM users WHERE username = 'admin'; -- 唯一索引3. eq_ref (关联查询最优)
-- JOIN 时使用主键或唯一索引
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- users 表的 type = eq_ref(使用主键)4. ref (较好)
-- 非唯一索引等值查询,可能返回多行
EXPLAIN SELECT * FROM orders WHERE status = 'PAID'; -- status 有索引但不是唯一5. range (中等)
-- 索引范围扫描:>, <, >=, <=, BETWEEN, IN
EXPLAIN SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
EXPLAIN SELECT * FROM users WHERE age IN (20, 25, 30);6. index (较差)
-- 全索引扫描:遍历整个索引树
EXPLAIN SELECT id FROM orders; -- 如果 id 是主键(聚簇索引)
EXPLAIN SELECT status FROM orders; -- 如果 status 有索引7. ALL (最差)
-- 全表扫描:遍历整个表
EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2024; -- 索引失效
EXPLAIN SELECT * FROM orders; -- 无 WHERE 条件性能优化目标:
- 至少达到
range级别 - 关联查询至少达到
ref级别 - 避免
ALL全表扫描
possible_keys - 可能使用的索引
含义:查询可能使用的索引列表
示例:
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- possible_keys: idx_user_id, idx_status
-- 表示这两个索引可能被使用注意:
- 列出的是"可能"的索引,不一定会使用
- 为 NULL 表示没有可用索引
key - 实际使用的索引(重要)
含义:查询实际使用的索引
示例:
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- possible_keys: idx_user_id, idx_status
-- key: idx_user_id (实际使用了 idx_user_id)特殊情况:
key = NULL:未使用索引key = PRIMARY:使用了主键- 与
possible_keys不同:实际使用的索引是优化器选择的结果
优化提示:
- 如果
possible_keys有值但key为 NULL,可能是因为:- 数据量小,全表扫描更快
- 索引选择性差
- 索引不符合查询条件
key_len - 使用的索引长度
含义:使用的索引字节数,可以判断联合索引使用了哪些列
计算规则:
| 数据类型 | 字节数 | 说明 |
|---|---|---|
| INT | 4 | - |
| BIGINT | 8 | - |
| VARCHAR(N) | N×3 + 2 | UTF8MB4 编码,N 为字符数 |
| DATE | 3 | - |
| DATETIME | 8 | - |
| NULL | +1 | 允许 NULL 则额外加 1 |
示例:
-- 创建联合索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
-- 查询 1:只使用 user_id
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- key_len = 4 (INT 类型,user_id NOT NULL)
-- 查询 2:使用 user_id + status
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- key_len = 4 + 10 (status VARCHAR(3), UTF8MB4: 3*3+1=10)
-- 查询 3:使用全部三个字段
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID' AND created_at > '2024-01-01';
-- key_len = 4 + 10 + 8 (DATETIME)重要用途:判断联合索引的利用率
-- 联合索引:idx_user_status_created(user_id, status, created_at)
-- 最大 key_len = 4 + 10 + 8 = 22
-- 如果 key_len < 22,说明索引没有完全利用
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND created_at > '2024-01-01';
-- 跳过了 status,只用了 user_id
-- key_len = 4 (只用了第一列)ref - 索引比较的列
含义:表示索引列与哪些列或常量进行比较
常见值:
const:常量数据库名.表名.列名:关联的字段func:使用函数
示例:
-- 常量比较
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- ref = const
-- 关联查询
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- orders 表:ref = test.u.id (使用 users 表的 id)
-- users 表:ref = test.o.user_id (使用 orders 表的 user_id)rows - 预估扫描行数(重要)
含义:MySQL 估计需要扫描的行数,越小越好
示例:
-- 无索引:扫描全表
EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- rows = 100000 (全表扫描)
-- 有索引:大幅减少
EXPLAIN SELECT * FROM orders WHERE created_at >= '2024-01-01';
-- rows = 1520 (使用索引范围扫描)注意:
- 这是预估值,可能不准确(InnoDB 估计值误差较大)
- 是"扫描行数",不是"返回行数"
- 对于关联查询,每个表的 rows 相乘得到总成本
优化目标:
- rows 值越小越好
- 与实际返回行数对比,差异大说明索引选择性问题
filtered - 过滤百分比
含义:表示条件过滤后剩余记录的百分比(MySQL 5.7+)
示例:
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- rows = 1000, filtered = 10.00
-- 表示:扫描 1000 行,其中 10%(100 行)满足 status = 'PAID'计算:
- 实际返回行数 ≈ rows × (filtered / 100)
filtered越大越好(接近 100)
Extra - 额外信息(重要)
含义:包含不适合在其他列显示的额外信息,很多信息直接反映性能问题
重要字段值:
1. Using index (好)
-- 覆盖索引:查询的所有列都在索引中
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 100;
-- Extra: Using index
-- 不需要回表,性能最优2. Using where (正常)
-- WHERE 条件过滤
EXPLAIN SELECT * FROM users WHERE age > 30 AND username LIKE '%admin%';
-- Extra: Using where
-- MySQL 服务器层过滤,正常情况3. Using index condition (较好)
-- 索引下推(ICP):在存储引擎层过滤
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status LIKE 'P%';
-- Extra: Using index condition
-- MySQL 5.6+ 优化,减少回表次数4. Using temporary (差)
-- 使用临时表
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using temporary
-- 通常用于 GROUP BY, ORDER BY, 需要优化优化:
-- 创建索引避免临时表
CREATE INDEX idx_status ON orders(status);
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using index (变成覆盖索引)5. Using filesort (差)
-- 文件排序:无法使用索引排序
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
-- Extra: Using filesort
-- 需要在内存或磁盘排序,性能差优化:
-- 创建联合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at);
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
-- Extra: NULL (使用索引排序)6. Using join buffer (差)
-- 关联查询未使用索引,需要连接缓冲区
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_name = u.username;
-- Extra: Using join buffer (Block Nested Loop)
-- 表示 users 表没有使用索引,需要优化7. Using where; Using temporary; Using filesort (最差)
-- 三重组合:WHERE 过滤 + 临时表 + 文件排序
EXPLAIN SELECT status, COUNT(*) FROM orders WHERE user_id = 100 GROUP BY status ORDER BY COUNT(*) DESC;
-- Extra: Using where; Using temporary; Using filesort
-- 性能很差,需要重点优化8. Impossible WHERE
-- WHERE 条件永远为 FALSE
EXPLAIN SELECT * FROM users WHERE id = 1 AND id = 2;
-- Extra: Impossible WHERE9. Using union (索引合并)
-- OR 条件使用多个索引
EXPLAIN SELECT * FROM orders WHERE user_id = 100 OR status = 'PAID';
-- Extra: Using union(idx_user_id, idx_status); Using where
-- MySQL 5.0+ 支持索引合并10. Using MRR (优化)
-- Multi-Range Read 优化
EXPLAIN SELECT * FROM orders WHERE user_id IN (100, 200, 300);
-- Extra: Using index condition; Using MRR
-- MySQL 5.6+ 优化,减少随机 IO1.4 执行计划分析实战
案例 1:判断是否使用索引
-- 创建索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 查询执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- 分析:
-- type = ref:使用非唯一索引,较好
-- key = idx_user_id:确实使用了索引
-- rows = 100:扫描 100 行
-- Extra = NULL:无额外问题案例 2:联合索引最左匹配
-- 创建联合索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
-- 查询 1:符合最左匹配
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- key = idx_user_status_created, key_len = 4
-- 查询 2:符合最左匹配(使用前两列)
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- key = idx_user_status_created, key_len = 14
-- 查询 3:不符合最左匹配(跳过 user_id)
EXPLAIN SELECT * FROM orders WHERE status = 'PAID';
-- key = NULL, type = ALL(全表扫描)案例 3:避免文件排序
-- 问题 SQL
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;
-- Extra: Using filesort (需要排序)
-- 优化:创建联合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at);
-- 再次查看
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;
-- Extra: NULL (使用索引排序,无文件排序)二、慢查询日志配置与分析
2.1 慢查询日志概述
慢查询日志(Slow Query Log)记录了执行时间超过指定阈值的 SQL 语句,是定位性能问题的重要工具。
作用:
- 发现执行慢的 SQL
- 分析性能瓶颈
- 为优化提供依据
2.2 慢查询日志配置
查看当前配置
-- 查看慢查询日志开关
SHOW VARIABLES LIKE 'slow_query_log';
-- 查看慢查询时间阈值(单位:秒)
SHOW VARIABLES LIKE 'long_query_time';
-- 查看慢查询日志文件路径
SHOW VARIABLES LIKE 'slow_query_log_file';
-- 查看是否记录未使用索引的 SQL
SHOW VARIABLES LIKE 'log_queries_not_using_indexes';
-- 查看慢查询日志输出格式
SHOW VARIABLES LIKE 'log_output';配置参数详解
| 参数 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|
slow_query_log | OFF | 是否启用慢查询日志 | ON |
long_query_time | 10 | 慢查询时间阈值(秒) | 1-3 |
slow_query_log_file | host_name-slow.log | 日志文件路径 | 指定路径 |
log_queries_not_using_indexes | OFF | 是否记录未使用索引的 SQL | ON |
log_output | FILE | 日志输出方式(FILE/TABLE/NONE) | FILE |
min_examined_row_limit | 0 | 扫描行数阈值 | 100 |
开启慢查询日志
方式 1:临时配置(重启失效)
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置慢查询时间阈值为 2 秒
SET GLOBAL long_query_time = 2;
-- 记录未使用索引的 SQL
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 设置日志文件路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';方式 2:永久配置(修改配置文件)
编辑 my.cnf (Linux) 或 my.ini (Windows):
[mysqld]
# 开启慢查询日志
slow_query_log = 1
# 慢查询时间阈值(秒)
long_query_time = 2
# 日志文件路径
slow_query_log_file = /var/log/mysql/mysql-slow.log
# 记录未使用索引的 SQL
log_queries_not_using_indexes = 1
# 日志输出方式(FILE/TABLE)
log_output = FILE
# 扫描行数阈值(少于该值不记录)
min_examined_row_limit = 100重启 MySQL 生效:
# Linux
systemctl restart mysqld
# 或
service mysql restart方式 3:动态配置(推荐)
-- 动态配置,立即生效,重启后失效
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 同时修改配置文件实现永久生效配置注意事项
1. long_query_time 设置
-- 不要设置太大,否则无法捕获慢查询
SET GLOBAL long_query_time = 10; -- × 太大
-- 也不要设置太小,否则日志爆炸
SET GLOBAL long_query_time = 0.1; -- × 太小
-- 推荐值:1-3 秒
SET GLOBAL long_query_time = 2; -- √ 合理2. log_queries_not_using_indexes
-- 开启后会记录所有未使用索引的 SQL,日志量可能很大
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 配合 min_examined_row_limit 过滤
SET GLOBAL min_examined_row_limit = 100; -- 扫描少于 100 行不记录3. 日志输出方式
-- FILE:记录到文件(推荐,性能好)
SET GLOBAL log_output = 'FILE';
-- TABLE:记录到 mysql.slow_log 表
SET GLOBAL log_output = 'TABLE';
-- 查询表日志
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
-- FILE,TABLE:同时记录到文件和表
SET GLOBAL log_output = 'FILE,TABLE';2.3 慢查询日志分析
查看慢查询日志
方式 1:直接查看文件
# 查看最近的慢查询
tail -f /var/log/mysql/mysql-slow.log
# 查看慢查询数量
grep -c "Query_time" /var/log/mysql/mysql-slow.log
# 提取 SQL 语句
grep "Query_time" /var/log/mysql/mysql-slow.log -A 3方式 2:查询 mysql.slow_log 表
-- 查询最近的慢查询
SELECT
start_time,
user_host,
query_time,
lock_time,
rows_sent,
rows_examined,
db,
sql_text
FROM mysql.slow_log
ORDER BY start_time DESC
LIMIT 10;
-- 统计慢查询 TOP 10
SELECT
db,
sql_text,
COUNT(*) as exec_count,
AVG(query_time) as avg_time,
MAX(query_time) as max_time,
SUM(rows_examined) as total_rows
FROM mysql.slow_log
WHERE start_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY db, sql_text
ORDER BY avg_time DESC
LIMIT 10;使用 mysqldumpslow 分析工具
mysqldumpslow 是 MySQL 自带的慢查询日志分析工具,可以汇总相似的 SQL。
基本用法:
# 显示执行时间最长的 10 条 SQL
mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log
# 显示访问次数最多的 10 条 SQL
mysqldumpslow -s c -t 10 /var/log/mysql/mysql-slow.log
# 显示平均执行时间最长的 10 条 SQL
mysqldumpslow -s at -t 10 /var/log/mysql/mysql-slow.log
# 显示锁定时间最长的 10 条 SQL
mysqldumpslow -s l -t 10 /var/log/mysql/mysql-slow.log
# 显示返回记录数最多的 10 条 SQL
mysqldumpslow -s r -t 10 /var/log/mysql/mysql-slow.log参数说明:
| 参数 | 说明 |
|---|---|
| -s t | 按查询时间排序 |
| -s at | 按平均查询时间排序 |
| -s c | 按查询次数排序 |
| -s l | 按锁定时间排序 |
| -s r | 按返回记录数排序 |
| -s ar | 按平均返回记录数排序 |
| -t N | 显示前 N 条 |
| -g pattern | 正则匹配 SQL |
示例:
# 查找包含 "SELECT" 的慢查询 TOP 10
mysqldumpslow -s t -t 10 -g "SELECT" /var/log/mysql/mysql-slow.log
# 查询某个数据库的慢查询
mysqldumpslow -s t -t 10 -g "db_name" /var/log/mysql/mysql-slow.log输出示例:
Reading mysql slow query log from /var/log/mysql/mysql-slow.log
Count: 152 Time=8.23s (1251s) Lock=0.00s (0s) Rows=100.0 (15200), root[root]@localhost
SELECT * FROM orders WHERE status = 'S' ORDER BY created_at DESC LIMIT N
Count: 89 Time=5.12s (456s) Lock=0.00s (0s) Rows=50.0 (4450), root[root]@localhost
SELECT * FROM users WHERE username LIKE 'S' ORDER BY id DESC LIMIT N解读:
Count: 152:执行 152 次Time=8.23s:平均执行时间 8.23 秒(1251s):总执行时间 1251 秒Rows=100.0:平均返回 100 行S:参数被替换为通配符
使用 pt-query-digest 分析工具(推荐)
pt-query-digest 是 Percona Toolkit 的一部分,功能更强大。
安装:
# Ubuntu/Debian
sudo apt-get install percona-toolkit
# CentOS/RHEL
sudo yum install percona-toolkit
# macOS
brew install percona-toolkit基本用法:
# 分析慢查询日志
pt-query-digest /var/log/mysql/mysql-slow.log
# 分析最近 1 小时的慢查询
pt-query-digest --since '1h' /var/log/mysql/mysql-slow.log
# 分析某个时间段的慢查询
pt-query-digest --since '2024-01-01 00:00:00' --until '2024-01-02 00:00:00' /var/log/mysql/mysql-slow.log
# 过滤某个数据库
pt-query-digest --filter '$event->{db} =~ /mydb/' /var/log/mysql/mysql-slow.log
# 输出到文件
pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txt输出示例:
# 3600ms user time, 20ms system time, 24.73M rss, 204.84M vsz
# Current date: Mon Mar 30 12:00:00 2024
# Hostname: mysql-server
# Files: /var/log/mysql/mysql-slow.log
# Overall: 15.23k total, 152 unique, 45.23 QPS, 0.23x concurrency ________
# Time range: 2024-03-29T00:00:00 to 2024-03-30T00:00:00
# Attribute total min max avg 95% stddev median
# ============ ======= ======= ======= ======= ======= ======= =======
# Exec time 3726s 1us 125s 245ms 100ms 3s 50ms
# Lock time 12s 0 1s 787us 159us 12ms 50us
# Rows sent 10.23M 0 100.00k 704.33 50.00 4.12k 0.99
# Rows examine 512.34M 0 1.50M 35.29k 1.50M 123.45k 100.00
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== =================================== =============== ===== ====== ===== ===============
# 1 0x1234567890ABCDEF1234567890ABCDEF 1523.2345 40.9% 1523 1.0000 0.00 SELECT orders
# 2 0xFEDCBA0987654321FEDCBA0987654321 823.4567 22.1% 823 1.0000 0.00 SELECT users
# 3 0xABCDEF1234567890ABCDEF1234567890 512.3456 13.8% 512 1.0000 0.00 SELECT products
# Query 1: 45.23 QPS, 0.23x concurrency, ID 0x1234567890ABCDEF1234567890ABCDEF at byte 0
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: 2024-03-29T00:00:00 to 2024-03-30T00:00:00
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 10 1523
# Exec time 40 1523s 1s 5s 1s 3s 500ms 1s
# Lock time 0 152ms 50us 100us 100us 100us 10us 100us
# Rows sent 1 152.30k 100 100 100 100 0 100
# Rows examine 29 152.30M 100.00k 100.00k 100.00k 100.00k 0 100.00k
# Query_time distribution
# 1us
# 10us
# 100us
# 1ms
# 10ms
# 100ms ################################
# 1s ################################################################
# 10s
# 100s
# 1s
# 10s
# Tables
# SHOW TABLE STATUS LIKE 'orders'\G
# SHOW CREATE TABLE `orders`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT * FROM orders WHERE status = 'PAID' ORDER BY created_at DESC LIMIT 100\G解读:
Response time 40.9%:该 SQL 占总响应时间的 40.9%,优先优化Rows examine 152.30M:扫描了 152.30M 行,但只返回 152.30k 行,效率低Query_time distribution:执行时间分布,大部分在 1 秒左右
2.4 实时监控慢查询
使用 performance_schema(MySQL 5.7+)
-- 开启事件监控
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE '%statement/%';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE '%events_statements_%';
-- 查询执行时间最长的 SQL
SELECT
DIGEST_TEXT as sql_text,
COUNT_STAR as exec_count,
SUM_TIMER_WAIT/1000000000 as total_time_sec,
AVG_TIMER_WAIT/1000000000 as avg_time_sec,
MAX_TIMER_WAIT/1000000000 as max_time_sec,
SUM_ROWS_EXAMINED as rows_examined,
SUM_ROWS_SENT as rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 查询全表扫描的 SQL
SELECT
DIGEST_TEXT as sql_text,
COUNT_STAR as exec_count,
SUM_NO_INDEX_USED as no_index_count,
SUM_ROWS_EXAMINED as rows_examined
FROM performance_schema.events_statements_summary_by_digest
WHERE SUM_NO_INDEX_USED > 0
ORDER BY SUM_ROWS_EXAMINED DESC
LIMIT 10;使用 sys schema(MySQL 5.7+)
-- 查看执行时间最长的 SQL
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile LIMIT 10;
-- 查看全表扫描的 SQL
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
-- 查看使用临时表的 SQL
SELECT * FROM sys.statements_with_temp_tables LIMIT 10;
-- 查看最新的慢查询
SELECT * FROM sys.latest_file_io ORDER BY latency DESC LIMIT 10;三、索引优化建议
3.1 索引设计原则
最左匹配原则
联合索引按照定义顺序从左到右匹配,查询条件必须包含索引的最左列。
-- 创建联合索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
-- √ 命中索引(使用全部列)
SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID' AND created_at > '2024-01-01';
-- √ 命中索引(使用前两列)
SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- √ 命中索引(只使用第一列)
SELECT * FROM orders WHERE user_id = 100;
-- × 不命中索引(跳过 user_id)
SELECT * FROM orders WHERE status = 'PAID';
-- × 不命中索引(跳过前两列)
SELECT * FROM orders WHERE created_at > '2024-01-01';
-- 部分命中(只使用第一列,跳过 status)
SELECT * FROM orders WHERE user_id = 100 AND created_at > '2024-01-01';
-- key_len 显示只用了 user_id设计建议:
- 联合索引列顺序:等值查询列 > 范围查询列 > 排序列
- 高选择性列放前面(区分度高)
- 最常用的查询条件放最左
覆盖索引
查询的所有列都在索引中,不需要回表查询。
-- 创建联合索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
-- × 需要回表
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- Extra: NULL (需要回表查询其他列)
-- √ 覆盖索引
EXPLAIN SELECT user_id, status, created_at FROM orders WHERE user_id = 100;
-- Extra: Using index (不需要回表)
-- √ 部分覆盖索引
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 100;
-- Extra: Using index (也不需要回表)优化建议:
- 尽量使用覆盖索引,减少回表
- SELECT 只查询需要的列
- 统计查询尽量使用覆盖索引
-- × 性能差
SELECT COUNT(*) FROM orders WHERE user_id = 100;
-- √ 使用覆盖索引
CREATE INDEX idx_user_id ON orders(user_id);
SELECT COUNT(user_id) FROM orders WHERE user_id = 100;
-- Extra: Using index索引选择性
索引列的区分度,选择性越高,索引效果越好。
计算公式:
-- 选择性 = COUNT(DISTINCT column) / COUNT(*)
SELECT
COUNT(DISTINCT user_id) / COUNT(*) as user_id_selectivity,
COUNT(DISTINCT status) / COUNT(*) as status_selectivity,
COUNT(DISTINCT created_at) / COUNT(*) as created_at_selectivity
FROM orders;示例:
-- 假设 orders 表有 100 万行数据
-- user_id: 10 万个不同值,选择性 = 100000/1000000 = 0.1 (高)
-- status: 5 个不同值,选择性 = 5/1000000 = 0.000005 (低)
-- created_at: 365 个不同值,选择性 = 365/1000000 = 0.000365 (中)
-- 索引设计:选择性高的列放前面
CREATE INDEX idx_user_created_status ON orders(user_id, created_at, status);判断索引是否有用:
-- 查看某列的选择性
SELECT
COUNT(*) as total_rows,
COUNT(DISTINCT status) as distinct_values,
COUNT(DISTINCT status) / COUNT(*) as selectivity
FROM orders;
-- 选择性 < 0.1:索引效果差
-- 选择性 > 0.5:索引效果好
-- 选择性 > 0.8:索引效果很好3.2 避免索引失效
1. 避免在索引列上使用函数
-- × 索引失效:使用函数
EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2024;
-- type = ALL (全表扫描)
-- √ 索引生效:范围查询
EXPLAIN SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- type = range常见错误:
-- × 使用函数
WHERE DATE(created_at) = '2024-03-30'
WHERE UPPER(username) = 'ADMIN'
WHERE SUBSTRING(order_no, 1, 4) = '2024'
-- √ 优化
WHERE created_at >= '2024-03-30 00:00:00' AND created_at < '2024-03-31 00:00:00'
WHERE username = 'ADMIN' OR username = 'admin'
WHERE order_no LIKE '2024%'2. 避免隐式类型转换
-- 假设 user_id 是 INT 类型,order_no 是 VARCHAR 类型
-- × 隐式转换:字符串转数字
EXPLAIN SELECT * FROM orders WHERE user_id = '100';
-- 虽然可以工作,但性能稍差
-- × 隐式转换:数字转字符串,索引失效
EXPLAIN SELECT * FROM orders WHERE order_no = 20240330001;
-- type = ALL (全表扫描)
-- √ 类型匹配
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
EXPLAIN SELECT * FROM orders WHERE order_no = '20240330001';常见错误:
-- × 隐式转换
WHERE user_id = '100' -- INT 列用字符串
WHERE order_no = 123 -- VARCHAR 列用数字
WHERE created_at = '2024-03-30' -- DATETIME 列用 DATE
-- √ 类型匹配
WHERE user_id = 100
WHERE order_no = '123'
WHERE created_at = '2024-03-30 00:00:00'3. 避免使用 != 或 <>
-- × 索引可能失效
EXPLAIN SELECT * FROM orders WHERE status != 'CANCELLED';
-- type = ALL 或 range(取决于数据分布)
-- √ 改用 IN
EXPLAIN SELECT * FROM orders WHERE status IN ('PAID', 'SHIPPED', 'COMPLETED');
-- type = range4. 避免使用 OR 连接不同字段
-- × 索引失效(MySQL 5.6 之前)
EXPLAIN SELECT * FROM orders WHERE user_id = 100 OR status = 'PAID';
-- type = ALL
-- √ 使用 UNION
EXPLAIN
SELECT * FROM orders WHERE user_id = 100
UNION
SELECT * FROM orders WHERE status = 'PAID';
-- 各自使用索引
-- √ MySQL 5.6+ 支持索引合并
EXPLAIN SELECT * FROM orders WHERE user_id = 100 OR status = 'PAID';
-- type = index_merge (使用两个索引)5. 避免 LIKE 以通配符开头
-- × 索引失效
EXPLAIN SELECT * FROM users WHERE username LIKE '%admin%';
-- type = ALL
-- × 索引失效
EXPLAIN SELECT * FROM users WHERE username LIKE '%admin';
-- type = ALL
-- √ 索引生效
EXPLAIN SELECT * FROM users WHERE username LIKE 'admin%';
-- type = range
-- 解决方案:使用全文索引
ALTER TABLE users ADD FULLTEXT INDEX idx_username(username);
SELECT * FROM users WHERE MATCH(username) AGAINST('admin');6. 避免 IS NULL / IS NOT NULL
-- × 可能不使用索引(取决于数据分布)
EXPLAIN SELECT * FROM orders WHERE status IS NULL;
-- type = ref 或 ALL
-- √ 优化:设置默认值
ALTER TABLE orders MODIFY status VARCHAR(20) NOT NULL DEFAULT 'PENDING';
EXPLAIN SELECT * FROM orders WHERE status = 'PENDING';
-- type = ref7. 避免范围查询后的索引列失效
-- 创建联合索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
-- × 范围查询后的列索引失效
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status > 'A' AND created_at > '2024-01-01';
-- key_len 显示只用了 user_id + status,created_at 索引失效
-- √ 优化:调整索引顺序
CREATE INDEX idx_user_created_status ON orders(user_id, created_at, status);
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND created_at > '2024-01-01' AND status > 'A';
-- 三个字段都能使用索引3.3 索引优化实战
案例 1:优化 ORDER BY
-- 问题 SQL
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;
-- Extra: Using filesort (文件排序)
-- 优化:创建联合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at);
-- 再次查看
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;
-- Extra: NULL (使用索引排序)
-- 进一步优化:覆盖索引
EXPLAIN SELECT order_id, user_id, created_at FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;
-- Extra: Using index (覆盖索引,最优)案例 2:优化 GROUP BY
-- 问题 SQL
EXPLAIN SELECT status, COUNT(*) FROM orders WHERE user_id = 100 GROUP BY status;
-- Extra: Using temporary; Using filesort (临时表 + 排序)
-- 优化:创建联合索引
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 再次查看
EXPLAIN SELECT status, COUNT(*) FROM orders WHERE user_id = 100 GROUP BY status;
-- Extra: Using index (覆盖索引,无需临时表)案例 3:优化分页查询
-- 问题 SQL:深度分页
EXPLAIN SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
-- rows = 100010 (扫描大量数据)
-- 优化 1:使用子查询
EXPLAIN SELECT * FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t
ON o.id = t.id;
-- 先查询主键(覆盖索引),再关联查询,性能更好
-- 优化 2:记录上次的最大 ID
EXPLAIN SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;
-- rows = 10 (使用范围查询)四、常见慢查询场景和优化方法
4.1 全表扫描
现象:type = ALL,扫描整个表
原因:
- 无索引
- 索引失效
- MySQL 认为全表扫描更快
优化方法:
-- 案例:按用户名查询
EXPLAIN SELECT * FROM users WHERE username = 'admin';
-- type = ALL
-- 优化:创建索引
CREATE INDEX idx_username ON users(username);
EXPLAIN SELECT * FROM users WHERE username = 'admin';
-- type = ref4.2 回表查询过多
现象:Extra = NULL,使用了索引但需要回表
优化方法:使用覆盖索引
-- 案例:查询用户 ID 和状态
EXPLAIN SELECT user_id, status, created_at FROM orders WHERE user_id = 100;
-- Extra: NULL (需要回表)
-- 优化:创建覆盖索引
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
EXPLAIN SELECT user_id, status, created_at FROM orders WHERE user_id = 100;
-- Extra: Using index (覆盖索引)4.3 文件排序
现象:Extra = Using filesort
原因:
- ORDER BY 字段无索引
- ORDER BY 字段与索引顺序不一致
优化方法:
-- 案例:按创建时间排序
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
-- Extra: Using filesort
-- 优化:创建联合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at);
EXPLAIN SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
-- Extra: NULL4.4 临时表
现象:Extra = Using temporary
原因:
- GROUP BY 字段无索引
- DISTINCT 查询
- UNION 查询
优化方法:
-- 案例:按状态分组统计
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using temporary
-- 优化:创建索引
CREATE INDEX idx_status ON orders(status);
EXPLAIN SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using index4.5 索引选择错误
现象:有多个可用索引,但 MySQL 选择了不合适的索引
优化方法:
-- 案例:MySQL 选择了错误的索引
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'PAID';
-- possible_keys: idx_user_id, idx_status
-- key: idx_status (选择了错误的索引)
-- 优化 1:使用 FORCE INDEX 强制使用索引
EXPLAIN SELECT * FROM orders FORCE INDEX(idx_user_id) WHERE user_id = 100 AND status = 'PAID';
-- 优化 2:使用 USE INDEX 建议 MySQL 使用索引
EXPLAIN SELECT * FROM orders USE INDEX(idx_user_id) WHERE user_id = 100 AND status = 'PAID';
-- 优化 3:创建更合适的联合索引
CREATE INDEX idx_user_status ON orders(user_id, status);4.6 关联查询性能差
现象:关联查询慢,Extra = Using join buffer
原因:
- 关联字段无索引
- 关联字段类型不匹配
优化方法:
-- 案例:关联查询
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- users 表: type = eq_ref (使用主键,好)
-- orders 表: type = ALL (无索引,差)
-- 优化:为关联字段创建索引
CREATE INDEX idx_user_id ON orders(user_id);
EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id;
-- orders 表: type = ref4.7 深度分页
现象:LIMIT offset 很大时查询慢
原因:MySQL 需要扫描 offset + limit 行数据
优化方法:
-- 问题 SQL:深度分页
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 10;
-- 扫描 100010 行
-- 优化 1:使用子查询
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 10) t
ON o.id = t.id;
-- 子查询使用覆盖索引,性能更好
-- 优化 2:记录上次的最大值
SELECT * FROM orders
WHERE created_at < '2024-03-30 12:00:00'
ORDER BY created_at DESC
LIMIT 10;
-- 使用范围查询,无需扫描大量数据
-- 优化 3:业务限制(禁止深度分页)
-- 只允许查看前 100 页
SELECT * FROM orders ORDER BY created_at DESC LIMIT 1000;4.8 OR 条件优化
现象:使用 OR 导致索引失效
优化方法:
-- 问题 SQL
SELECT * FROM orders WHERE user_id = 100 OR status = 'PAID';
-- MySQL 5.6 之前:全表扫描
-- MySQL 5.6+:索引合并(index_merge),性能中等
-- 优化 1:使用 UNION
SELECT * FROM orders WHERE user_id = 100
UNION
SELECT * FROM orders WHERE status = 'PAID';
-- 各自使用索引,性能好
-- 优化 2:创建联合索引(如果条件固定)
CREATE INDEX idx_user_status ON orders(user_id, status);
SELECT * FROM orders WHERE user_id = 100 OR status = 'PAID';
-- 仍然可以使用索引合并,性能更好五、性能分析工具使用
5.1 SHOW PROFILE
SHOW PROFILE 是 MySQL 提供的 SQL 性能分析工具,可以查看 SQL 执行的详细耗时。
启用 PROFILE
-- 查看是否启用
SHOW VARIABLES LIKE 'profiling';
-- 启用 profiling
SET profiling = 1;
-- 或设置保留的历史记录数
SET profiling_history_size = 100;使用 PROFILE 分析
-- 执行 SQL
SELECT * FROM orders WHERE user_id = 100;
-- 查看最近的 SQL 列表
SHOW PROFILES;
-- 查看指定 SQL 的详细耗时(Query_ID 来自 SHOW PROFILES)
SHOW PROFILE FOR QUERY 1;
-- 查看所有性能指标
SHOW PROFILE ALL FOR QUERY 1;
-- 查看特定指标
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;PROFILE 输出示例
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000100 |
| checking permissions | 0.000010 |
| Opening tables | 0.000020 |
| init | 0.000030 |
| System lock | 0.000010 |
| optimizing | 0.000010 |
| statistics | 0.000050 |
| preparing | 0.000020 |
| executing | 0.000005 |
| Sending data | 0.500000 | ← 主要耗时
| end | 0.000010 |
| query end | 0.000005 |
| closing tables | 0.000010 |
| freeing items | 0.000020 |
| cleaning up | 0.000010 |
+----------------------+----------+关键指标:
Sending data:发送数据,通常是查询和传输数据的主要耗时Sorting result:排序耗时Creating tmp table:创建临时表Copying to tmp table:拷贝数据到临时表System lock:系统锁等待Table lock:表锁等待
注意:MySQL 5.7 后 SHOW PROFILE 被标记为过时,推荐使用 performance_schema。
5.2 Performance Schema
MySQL 5.5+ 提供的性能监控框架,可以监控 MySQL 的各种内部操作。
查看等待事件
-- 启用等待事件监控
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE '%wait/%';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME LIKE '%events_waits_%';
-- 查看等待事件
SELECT
EVENT_NAME,
COUNT_STAR as count,
SUM_TIMER_WAIT/1000000000 as total_sec,
AVG_TIMER_WAIT/1000000000 as avg_sec
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE SUM_TIMER_WAIT > 0
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;查看 SQL 执行统计
-- 查看 SQL 执行统计
SELECT
DIGEST_TEXT as sql_text,
COUNT_STAR as exec_count,
SUM_TIMER_WAIT/1000000000 as total_sec,
AVG_TIMER_WAIT/1000000000 as avg_sec,
MAX_TIMER_WAIT/1000000000 as max_sec,
SUM_ROWS_EXAMINED as rows_examined,
SUM_ROWS_SENT as rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;查看文件 IO 统计
-- 查看文件 IO 统计
SELECT
FILE_NAME,
COUNT_READ,
COUNT_WRITE,
SUM_NUMBER_OF_BYTES_READ/1024/1024 as read_mb,
SUM_NUMBER_OF_BYTES_WRITE/1024/1024 as write_mb
FROM performance_schema.file_summary_by_instance
ORDER BY SUM_NUMBER_OF_BYTES_READ + SUM_NUMBER_OF_BYTES_WRITE DESC
LIMIT 10;5.3 Sys Schema
MySQL 5.7+ 提供的系统视图,简化了 performance_schema 的查询。
常用视图
-- 查看执行时间最长的 SQL
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile LIMIT 10;
-- 查看全表扫描的 SQL
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
-- 查看使用临时表或文件排序的 SQL
SELECT * FROM sys.statements_with_temp_tables LIMIT 10;
-- 查看冗余索引
SELECT * FROM sys.schema_redundant_indexes;
-- 查看未使用的索引
SELECT * FROM sys.schema_unused_indexes;
-- 查看表统计信息
SELECT * FROM sys.schema_table_statistics LIMIT 10;
-- 查看 IO 统计
SELECT * FROM sys.io_global_by_file_by_bytes LIMIT 10;
-- 查看内存使用
SELECT * FROM sys.memory_global_by_current_bytes LIMIT 10;5.4 MySQL Workbench
MySQL 官方提供的图形化管理工具,包含性能分析功能。
功能:
- 可视化执行计划
- 性能仪表板
- 慢查询日志分析
- 配置管理
5.5 其他工具
pt-index-usage
分析慢查询日志,找出未使用的索引。
pt-index-usage /var/log/mysql/mysql-slow.log --host=localhost --user=root --password=123456pt-mysql-summary
生成 MySQL 配置和状态摘要。
pt-mysql-summary --host=localhost --user=root --password=123456MySQL Enterprise Monitor
MySQL 企业版提供的监控工具,需要付费。
六、慢查询优化实战案例
6.1 案例 1:电商订单查询优化
业务场景:查询某个用户的订单列表,按创建时间倒序分页。
问题 SQL:
SELECT * FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 0, 20;执行计划分析:
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 0, 20;
-- 结果:
-- type: ref (使用索引 idx_user_id)
-- key: idx_user_id
-- rows: 1520
-- Extra: Using filesort (文件排序)问题:
- 使用了索引
idx_user_id,但需要回表查询所有字段 ORDER BY created_at DESC导致文件排序- 扫描行数较多(1520 行)
优化方案:
-- 方案 1:创建联合索引
CREATE INDEX idx_user_created ON orders(user_id, created_at);
-- 再次查看执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 ORDER BY created_at DESC LIMIT 0, 20;
-- type: ref
-- key: idx_user_created
-- Extra: NULL (无文件排序)
-- 方案 2:使用覆盖索引(如果只需要部分字段)
EXPLAIN SELECT order_id, user_id, created_at, status
FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 0, 20;
-- Extra: Using index (覆盖索引)性能对比:
| 优化前 | 优化后 | 提升 |
|---|---|---|
| 执行时间: 50ms | 执行时间: 5ms | 10倍 |
| 扫描行数: 1520 | 扫描行数: 20 | 76倍 |
| 文件排序: 是 | 文件排序: 否 | √ |
6.2 案例 2:社交平台动态查询优化
业务场景:查询用户关注的动态,按时间倒序展示。
问题 SQL:
SELECT d.*
FROM dynamics d
JOIN user_follows f ON d.user_id = f.follow_user_id
WHERE f.user_id = 1001
ORDER BY d.created_at DESC
LIMIT 0, 20;执行计划分析:
EXPLAIN SELECT d.* FROM dynamics d JOIN user_follows f ON d.user_id = f.follow_user_id WHERE f.user_id = 1001 ORDER BY d.created_at DESC LIMIT 0, 20;
-- 结果:
-- dynamics 表: type: ALL (全表扫描)
-- Extra: Using temporary; Using filesort (临时表 + 文件排序)问题:
dynamics表全表扫描- 使用了临时表和文件排序
- 关联字段
user_id无索引
优化方案:
-- 方案 1:创建索引
CREATE INDEX idx_user_created ON dynamics(user_id, created_at);
CREATE INDEX idx_user_id ON user_follows(user_id);
-- 再次查看执行计划
EXPLAIN SELECT d.* FROM dynamics d JOIN user_follows f ON d.user_id = f.follow_user_id WHERE f.user_id = 1001 ORDER BY d.created_at DESC LIMIT 0, 20;
-- dynamics 表: type: ref
-- Extra: Using filesort (仍有文件排序)
-- 方案 2:改写 SQL,先查关注列表
SELECT d.* FROM dynamics d
WHERE d.user_id IN (
SELECT follow_user_id FROM user_follows WHERE user_id = 1001
)
ORDER BY d.created_at DESC
LIMIT 0, 20;
-- 子查询使用索引,但需要创建临时表
-- 方案 3:使用冗余字段(推荐)
-- 在 dynamics 表中添加冗余字段:is_followed(是否被关注)
-- 或使用消息队列推送动态到用户的 feed 表最终方案:业务重构,使用推拉结合的 Feed 流架构。
6.3 案例 3:报表统计查询优化
业务场景:统计每个状态的订单数量和总金额。
问题 SQL:
SELECT
status,
COUNT(*) as order_count,
SUM(amount) as total_amount
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY status;执行计划分析:
EXPLAIN SELECT status, COUNT(*) as order_count, SUM(amount) as total_amount FROM orders WHERE created_at >= '2024-01-01' GROUP BY status;
-- 结果:
-- type: ALL (全表扫描)
-- Extra: Using temporary; Using filesort问题:
- 全表扫描
- 使用临时表和文件排序
SUM(amount)需要回表查询
优化方案:
-- 方案 1:创建联合索引
CREATE INDEX idx_created_status ON orders(created_at, status, amount);
-- 再次查看执行计划
EXPLAIN SELECT status, COUNT(*) as order_count, SUM(amount) as total_amount FROM orders WHERE created_at >= '2024-01-01' GROUP BY status;
-- type: range
-- Extra: Using index; Using temporary (使用覆盖索引,但仍有临时表)
-- 方案 2:使用物化视图(MySQL 不支持,可用定时任务替代)
CREATE TABLE order_stats_daily (
stat_date DATE,
status VARCHAR(20),
order_count INT,
total_amount DECIMAL(10,2),
PRIMARY KEY(stat_date, status)
);
-- 定时任务每天统计一次
INSERT INTO order_stats_daily
SELECT
CURDATE(),
status,
COUNT(*),
SUM(amount)
FROM orders
WHERE created_at >= CURDATE()
GROUP BY status
ON DUPLICATE KEY UPDATE
order_count = VALUES(order_count),
total_amount = VALUES(total_amount);
-- 查询时直接从统计表读取
SELECT * FROM order_stats_daily WHERE stat_date = '2024-03-30';6.4 案例 4:模糊搜索优化
业务场景:搜索商品名称包含"手机"的商品。
问题 SQL:
SELECT * FROM products WHERE name LIKE '%手机%';执行计划分析:
EXPLAIN SELECT * FROM products WHERE name LIKE '%手机%';
-- 结果:
-- type: ALL (全表扫描)
-- Extra: Using where问题:
%手机%导致索引失效- 全表扫描
优化方案:
-- 方案 1:使用全文索引
ALTER TABLE products ADD FULLTEXT INDEX idx_name(name);
SELECT * FROM products WHERE MATCH(name) AGAINST('手机');
-- 使用全文索引,性能好
-- 方案 2:使用 Elasticsearch
-- 将商品数据同步到 ES,使用 ES 进行搜索
-- 适合大规模数据搜索
-- 方案 3:使用 LIKE '手机%' (如果业务允许)
SELECT * FROM products WHERE name LIKE '手机%';
-- 可以使用索引6.5 案例 5:大表更新优化
业务场景:批量更新订单状态。
问题 SQL:
UPDATE orders SET status = 'CANCELLED' WHERE created_at < '2023-01-01';问题:
- 锁表时间过长
- 影响其他查询
- 可能导致主从延迟
优化方案:
-- 方案 1:分批更新
-- 每次更新 1000 条
UPDATE orders SET status = 'CANCELLED'
WHERE created_at < '2023-01-01' AND status != 'CANCELLED'
LIMIT 1000;
-- 循环执行,直到影响行数为 0
-- 脚本示例(Python):
"""
affected_rows = 1
while affected_rows > 0:
cursor.execute('''
UPDATE orders SET status = 'CANCELLED'
WHERE created_at < '2023-01-01' AND status != 'CANCELLED'
LIMIT 1000
''')
affected_rows = cursor.rowcount
connection.commit()
time.sleep(1) # 休息 1 秒,降低影响
"""
-- 方案 2:使用存储过程
DELIMITER //
CREATE PROCEDURE batch_update_orders()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE affected_rows INT DEFAULT 1;
WHILE affected_rows > 0 DO
UPDATE orders SET status = 'CANCELLED'
WHERE created_at < '2023-01-01' AND status != 'CANCELLED'
LIMIT 1000;
SET affected_rows = ROW_COUNT();
DO SLEEP(1);
END WHILE;
END //
DELIMITER ;
CALL batch_update_orders();
-- 方案 3:归档历史数据
-- 创建归档表
CREATE TABLE orders_archive LIKE orders;
-- 迁移数据
INSERT INTO orders_archive SELECT * FROM orders WHERE created_at < '2023-01-01';
-- 删除原表数据
DELETE FROM orders WHERE created_at < '2023-01-01';七、常见问题和解决方案
7.1 索引建立了但不生效
原因:
- 索引选择性太低
- 隐式类型转换
- 使用函数
- MySQL 优化器认为全表扫描更快
排查方法:
-- 查看 possible_keys 和 key
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- possible_keys: idx_user_id
-- key: NULL (未使用索引)
-- 原因 1:选择性太低
SELECT COUNT(DISTINCT user_id) / COUNT(*) FROM orders;
-- 结果 < 0.1,选择性太低
-- 原因 2:隐式转换
SHOW CREATE TABLE orders;
-- user_id INT
SELECT * FROM orders WHERE user_id = '100'; -- 字符串
-- 原因 3:使用函数
SELECT * FROM orders WHERE DATE(created_at) = '2024-03-30';
-- 原因 4:数据量小
SELECT COUNT(*) FROM orders; -- 数据量很小解决方案:
- 提高索引选择性
- 避免隐式转换
- 避免使用函数
- 使用 FORCE INDEX 强制使用索引
7.2 慢查询日志不记录
原因:
- 慢查询日志未开启
long_query_time设置太大- SQL 执行时间未超过阈值
- 日志文件权限问题
排查方法:
-- 检查配置
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'slow_query_log_file';
-- 测试慢查询
SELECT SLEEP(3); -- 执行 3 秒
-- 查看是否记录到日志解决方案:
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 检查日志文件权限
-- Linux:
-- ls -l /var/log/mysql/mysql-slow.log
-- chown mysql:mysql /var/log/mysql/mysql-slow.log
-- chmod 644 /var/log/mysql/mysql-slow.log7.3 优化后性能反而下降
原因:
- 索引过多,影响写入性能
- 统计信息不准确
- MySQL 选择了错误的执行计划
排查方法:
-- 查看索引数量
SHOW INDEX FROM orders;
-- 查看表统计信息
SHOW TABLE STATUS LIKE 'orders';
-- 分析表(更新统计信息)
ANALYZE TABLE orders;
-- 查看执行计划
EXPLAIN SELECT * FROM orders WHERE ...;解决方案:
- 删除冗余索引
- 更新统计信息
- 使用 FORCE INDEX 强制使用索引
7.4 分页查询越往后越慢
原因:MySQL 需要扫描 offset + limit 行数据
示例:
-- 第 1 页:扫描 10 行
SELECT * FROM orders ORDER BY id LIMIT 0, 10;
-- 第 10000 页:扫描 100010 行
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;解决方案:
-- 方案 1:使用子查询
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t
ON o.id = t.id;
-- 方案 2:记录上次的最大 ID
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;
-- 方案 3:限制分页深度(业务层面)
-- 只允许查看前 100 页7.5 COUNT(*) 查询慢
原因:InnoDB 需要扫描全表统计行数
解决方案:
-- 方案 1:使用覆盖索引
CREATE INDEX idx_status ON orders(status);
SELECT COUNT(status) FROM orders; -- 使用覆盖索引
-- 方案 2:缓存计数
CREATE TABLE table_counts (
table_name VARCHAR(50) PRIMARY KEY,
row_count BIGINT
);
INSERT INTO table_counts VALUES ('orders', 0);
-- 每次插入/删除时更新
INSERT INTO orders (...) VALUES (...);
UPDATE table_counts SET row_count = row_count + 1 WHERE table_name = 'orders';
-- 查询时直接读取
SELECT row_count FROM table_counts WHERE table_name = 'orders';
-- 方案 3:使用 Redis 缓存计数
-- 每次插入/删除时更新 Redis 计数器八、面试要点
8.1 基础问题
Q1:什么是执行计划?如何查看?
A:执行计划是数据库查询优化器根据 SQL 语句生成的执行方案,描述了数据库如何检索数据。使用 EXPLAIN 命令查看,EXPLAIN ANALYZE 可以看到实际执行统计。
Q2:EXPLAIN 中 type 字段有哪些值?性能从好到差排序。
A:从好到差依次为:
system:系统表,只有一行const:主键或唯一索引等值查询eq_ref:关联查询使用主键或唯一索引ref:非唯一索引等值查询range:索引范围扫描index:全索引扫描ALL:全表扫描
Q3:Extra 字段中出现 Using filesort 和 Using temporary 表示什么?如何优化?
A:
Using filesort:需要文件排序,无法使用索引排序。优化:创建合适的联合索引。Using temporary:需要创建临时表,通常用于 GROUP BY 或 DISTINCT。优化:创建索引,或使用覆盖索引。
Q4:什么是回表?如何避免?
A:回表是指通过非聚簇索引查找数据时,需要先查索引树找到主键值,再通过主键到聚簇索引查找完整记录。避免方法:使用覆盖索引,让查询的所有列都在索引中。
Q5:联合索引的最左匹配原则是什么?
A:联合索引按照定义顺序从左到右匹配,查询条件必须包含索引的最左列,否则索引失效。例如索引 (a, b, c),查询条件 WHERE b = 1 无法使用索引。
8.2 进阶问题
Q1:如何分析慢查询?
A:
- 开启慢查询日志,设置
long_query_time阈值 - 使用
mysqldumpslow或pt-query-digest分析慢查询日志 - 使用
EXPLAIN查看执行计划 - 使用
SHOW PROFILE查看详细耗时 - 分析是否是索引问题、锁等待、临时表等问题
- 针对性优化
Q2:什么情况下索引会失效?
A:
- 在索引列上使用函数:
WHERE YEAR(created_at) = 2024 - 隐式类型转换:
WHERE order_no = 123(order_no 是 VARCHAR) - 使用
!=或<> - 使用
OR连接不同字段(MySQL 5.6 之前) LIKE以通配符开头:WHERE name LIKE '%abc'IS NULL或IS NOT NULL(可能失效)- 范围查询后的索引列失效:
WHERE a = 1 AND b > 2 AND c = 3(c 索引失效)
Q3:如何优化深度分页查询?
A:
- 使用子查询:先查询主键 ID,再关联查询
sql
SELECT o.* FROM orders o JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t ON o.id = t.id; - 记录上次的最大 ID:使用范围查询代替 LIMIT offset
sql
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10; - 业务限制:只允许查看前 N 页
Q4:如何优化 ORDER BY?
A:
- 创建联合索引,让 ORDER BY 使用索引排序
sql
CREATE INDEX idx_user_created ON orders(user_id, created_at); SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC; - 使用覆盖索引,避免回表
- 减少 SELECT 字段,只查询需要的列
- 增加
sort_buffer_size参数(MySQL 配置)
Q5:如何判断索引是否有用?
A:
- 查看索引选择性:
COUNT(DISTINCT column) / COUNT(*),选择性越高越好(> 0.5) - 使用
EXPLAIN查看type和key字段 - 查看
rows字段,扫描行数越少越好 - 查看
Extra字段,是否使用了临时表或文件排序 - 对比优化前后的执行时间
8.3 实战问题
Q1:慢查询日志中有一条 SQL 执行 8 秒,但 EXPLAIN 看起来很正常,可能是什么原因?
A:
- 锁等待:SQL 本身执行很快,但等待锁释放耗时很长
- 排查方法:
SHOW ENGINE INNODB STATUS查看锁信息 - 或查看
performance_schema.data_locks表
- 排查方法:
- 数据量变化:执行时数据量大,现在数据量小
- 缓存未命中:执行时数据不在 buffer pool,需要从磁盘读取
- 并发压力:执行时系统负载高
- 统计信息不准确:优化器选择了错误的执行计划
Q2:表中有多个索引,MySQL 如何选择使用哪个?
A: MySQL 优化器基于成本模型选择索引,考虑因素包括:
- 索引选择性:区分度高的索引优先
- 扫描行数:预估扫描行数少的索引优先
- 索引类型:主键 > 唯一索引 > 普通索引
- 是否需要回表:覆盖索引优先
- 是否需要排序:可以使用索引排序的优先
可以查看执行计划的 possible_keys 和 key 字段,或使用 FORCE INDEX 强制使用指定索引。
Q3:如何优化 COUNT(*) 查询?
A:
- 使用覆盖索引:
sql
CREATE INDEX idx_status ON orders(status); SELECT COUNT(status) FROM orders; - 缓存计数:维护一个计数表或使用 Redis 缓存
- 预估行数:使用
SHOW TABLE STATUS查看预估行数 - 业务优化:避免实时 COUNT,改为定时统计
Q4:生产环境如何安全地添加索引?
A:
- 在从库上先创建索引,测试性能
- 使用
ALGORITHM=INPLACE, LOCK=NONE(MySQL 5.6+),避免锁表sqlALTER TABLE orders ADD INDEX idx_user_id(user_id), ALGORITHM=INPLACE, LOCK=NONE; - 在业务低峰期操作
- 使用
pt-online-schema-change工具(在线 DDL)bashpt-online-schema-change --alter "ADD INDEX idx_user_id(user_id)" D=mydb,t=orders --execute - 监控性能影响
Q5:如何监控和预警慢查询?
A:
- 定期分析慢查询日志,使用
pt-query-digest - 监控 MySQL 关键指标:
- 慢查询数量:
SHOW GLOBAL STATUS LIKE 'Slow_queries' - 查询缓存命中率(如果启用)
- 线程数:
SHOW STATUS LIKE 'Threads_connected'
- 慢查询数量:
- 使用监控工具:
- Prometheus + Grafana
- MySQL Enterprise Monitor
- Percona Monitoring and Management (PMM)
- 设置告警阈值:
- 慢查询数量 > N/小时
- 平均查询时间 > N 秒
- 定期审查和优化 Top N 慢查询
九、总结
核心要点
- 执行计划是 SQL 优化的基础工具,必须掌握
EXPLAIN各字段的含义 - 慢查询日志是发现问题的第一步,要合理配置和分析
- 索引不是万能的,有索引也可能慢,要理解索引的工作原理
- 优化要结合业务场景,不能脱离实际数据分布和访问模式
- 性能优化是持续过程,要建立监控和治理机制
优化流程
发现问题 → 定位问题 → 分析原因 → 制定方案 → 实施优化 → 验证效果 → 监控改进
↓ ↓ ↓ ↓ ↓ ↓ ↓
慢查询日志 EXPLAIN 索引/锁/SQL 创建索引 验证计划 对比性能 持续监控最佳实践
-
设计阶段:
- 合理设计索引,遵循最左匹配原则
- 高选择性列放联合索引前面
- 考虑覆盖索引,减少回表
-
开发阶段:
- 避免 SELECT *,只查询需要的列
- 避免 WHERE 条件使用函数或隐式转换
- 使用 EXPLAIN 验证执行计划
-
测试阶段:
- 使用生产数据量级测试
- 分析慢查询日志
- 压测验证性能
-
生产阶段:
- 监控慢查询
- 定期优化 Top N 慢查询
- 建立预警机制
常用命令速查
-- 查看执行计划
EXPLAIN SELECT ...;
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0.18+
-- 慢查询配置
SHOW VARIABLES LIKE 'slow_query_log';
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
-- 查看索引
SHOW INDEX FROM table_name;
-- 分析表(更新统计信息)
ANALYZE TABLE table_name;
-- 查看表状态
SHOW TABLE STATUS LIKE 'table_name';
-- 查看性能统计
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile LIMIT 10;
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
-- Profile 分析
SET profiling = 1;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;版本差异(MySQL 5.7 → 8.0/8.4)
| 特性 | 旧版(本文编写时,MySQL 5.7) | 当前(MySQL 8.0/8.4 LTS) |
|---|---|---|
| 默认字符集 | utf8(需显式配置 utf8mb4) | utf8mb4(MySQL 8.0 起默认) |
| 索引 | 普通 B+Tree | 降序索引、隐藏索引、函数索引(8.0+) |
| SQL 能力 | 常规查询 | 递归 CTE、窗口函数(8.0+) |
| 版本策略 | 5.7 | 8.0(主流)/ 8.4 LTS / 9.x(创新版) |
| Java 驱动 | mysql-connector-java 5.x/8.0 | mysql-connector-j 8.x/9.x |
本文基于 MySQL 5.7 编写,核心概念(索引、事务、锁、MVCC、InnoDB)在 8.0/8.4 中依然适用;8.0 的默认字符集、隐藏索引与 SQL 增强(CTE/窗口函数)是升级后的主要差异。