慢查询分析与性能调优
本教程共 46 篇 · 第 43 篇 · 更新于 2026-07-30 · 约 13 分钟阅读
43. 慢查询分析与性能调优
本节目标:学会开启和分析慢查询日志找到慢 SQL,掌握减少回表、优化 JOIN、深分页、COUNT 等常见调优手法,能把一条慢查询优化到合理水平。
43.1 慢查询日志
慢查询日志(Slow Query Log) 记录所有执行时间超过阈值的 SQL。这是发现慢查询的第一手段。
开启慢查询日志
-- 查看状态
SHOW VARIABLES LIKE 'slow_query_log%';
-- slow_query_log = OFF(默认关闭)
-- slow_query_log_file = /var/lib/mysql/hostname-slow.log
-- 开启
SET GLOBAL slow_query_log = ON;
-- 设置阈值(秒),默认 10 秒
SET GLOBAL long_query_time = 1; -- 超过 1 秒就记录
-- 设置日志文件位置
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
Note
SET GLOBAL只对当前运行实例有效,重启后失效。永久生效需要写入配置文件(my.cnf 或 my.ini),下一节会讲。
配置文件方式(永久生效)
[mysqld]
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = 1 -- 记录没走索引的查询
分析慢查询日志
-- 查看慢查询日志中有多少条记录
SHOW VARIABLES LIKE 'slow_query_log%';
-- 日志文件是文本文件,可以用 mysqldumpslow 工具分析
-- 命令行执行:
-- mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
-- -s t 按时间排序,-t 10 显示前 10 条
mysqldumpslow 常用参数:
| 参数 | 含义 |
|---|---|
-s t | 按总耗时排序 |
-s r | 按返回行数排序 |
-s c | 按出现次数排序 |
-t 10 | 只显示前 10 条 |
Tip我之前排查线上慢查询,用
mysqldumpslow -s t -t 10一跑,排第一的 SQL 占了总耗时的 60%。一条 SQL 优化好,整体性能就上去了。
43.2 调优基本流程
拿到一条慢 SQL 后,按以下步骤排查:
- EXPLAIN 分析:看 type、key、rows、Extra;
- 判断是否走索引:key 为 NULL 说明没走索引;
- 检查索引失效:函数操作、隐式转换、LIKE ‘%xxx’ 等;
- 检查扫描行数:rows 远大于实际需要说明索引选择性差;
- 检查 Extra:Using filesort、Using temporary 需要优化;
- 优化 SQL 或索引:加索引、改写 SQL、减少回表。
43.3 减少 SELECT * 和回表
-- 差:SELECT * 需要所有列,必然回表
SELECT * FROM users WHERE age = 25;
-- 好:只查需要的列,可能用上覆盖索引
SELECT id, username FROM users WHERE age = 25;
-- 如果有联合索引 (age, username),Extra: Using index,不回表
Warning
SELECT *是性能杀手。它不仅可能导致回表,还增加了网络传输量。养成只查需要列的习惯。
43.4 优化 JOIN 查询
JOIN 性能取决于被驱动表(JOIN 右边的表)是否走索引。
-- 差:orders 表的 user_id 没有索引,被驱动表全表扫描
EXPLAIN SELECT u.username, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;
-- o 表 type=ALL,Using join buffer
-- 好:给 orders.user_id 建索引
CREATE INDEX idx_uid ON orders(user_id);
EXPLAIN SELECT u.username, o.amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.age > 20;
-- o 表 type=ref,走索引
JOIN 优化原则:
- 被驱动表的 JOIN 条件列必须有索引;
- 小表驱动大表(小表做驱动表);
- JOIN 表数量不超过 3 张,多了拆分查询。
NoteMySQL 优化器会自动选择小表做驱动表(Nested Loop Join)。但如果两张表都很大,JOIN 效率会很差。考虑用 Hash Join(MySQL 8.0.18+ 自动选择)或分批查询。
43.5 深分页优化
-- 传统分页:LIMIT 偏移量很大时很慢
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
-- MySQL 要扫描前 100010 行,丢弃前 100000 行,只返回 10 行
偏移量越大,扫描的无效行越多,越慢。
优化方案一:延迟关联
-- 先通过覆盖索引查出主键,再关联回表
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 100000, 10
) t ON o.id = t.id;
-- 子查询走覆盖索引(只扫主键),再精确回表 10 行
优化方案二:记录上次最大 ID
-- 第一页
SELECT * FROM orders WHERE id > 0 ORDER BY id LIMIT 10;
-- 假设最后一条 id=10
-- 第二页:直接从上次最大 id 开始
SELECT * FROM orders WHERE id > 10 ORDER BY id LIMIT 10;
-- 不需要偏移,直接定位
Tip记录上次最大 ID 的方案性能最好,但只适用于连续翻页(不能跳页)。适合下拉加载更多的场景。如果必须跳页,用延迟关联。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 传统 LIMIT | 偏移量小 | 简单 | 深分页慢 |
| 延迟关联 | 必须跳页 | 减少回表 | SQL 稍复杂 |
| 记录上次 ID | 连续翻页 | 性能最优 | 不能跳页 |
43.6 COUNT 优化
-- COUNT(*) 统计总行数
SELECT COUNT(*) FROM users;
-- InnoDB 需要扫描整张表(或索引),大表很慢
InnoDB 的 COUNT(*) 没有 MyISAM 那样的计数器,需要实际扫描。优化思路:
-- 方案一:用 SHOW TABLE STATUS 估算(不精确但快)
SHOW TABLE STATUS LIKE 'users';
-- Rows 字段是估算值
-- 方案二:用缓存或汇总表
-- 维护一张统计表,定时更新
CREATE TABLE table_stats (table_name VARCHAR(50), row_count INT);
-- 业务层从统计表取值
-- 方案三:只查有索引的列
SELECT COUNT(id) FROM users; -- 走主键索引扫描,比 COUNT(*) 稍快
Note
COUNT(*)、COUNT(1)、COUNT(主键)在 InnoDB 中性能差不多,优化器会选择最小的索引扫描。COUNT(列)不统计 NULL 值,语义不同。需要精确总数的大表场景,建议用汇总表。
43.7 避免在索引列上运算
-- 差:索引列上用了函数
SELECT * FROM orders WHERE DATE(created_at) = '2026-07-30';
-- 索引失效,全表扫描
-- 好:改写为范围查询
SELECT * FROM orders
WHERE created_at >= '2026-07-30 00:00:00'
AND created_at < '2026-07-31 00:00:00';
-- 走索引范围扫描
-- 差:索引列上做运算
SELECT * FROM orders WHERE amount / 2 = 50;
-- 索引失效
-- 好:把运算移到等号右边
SELECT * FROM orders WHERE amount = 50 * 2;
-- 走索引
43.8 优化 GROUP BY 和 ORDER BY
-- 差:GROUP BY 的列没有索引
SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using temporary; Using filesort
-- 好:给 GROUP BY 的列建索引
CREATE INDEX idx_status ON orders(status);
SELECT status, COUNT(*) FROM orders GROUP BY status;
-- Extra: Using index(覆盖索引 + 索引有序,不需要临时表和排序)
ORDER BY 同理:
-- 差:ORDER BY 的列没有索引
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- Extra: Using filesort
-- 好:给排序列建索引
CREATE INDEX idx_created ON users(created_at);
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- 利用索引的有序性,不需要 filesort
WarningORDER BY 的方向必须和索引方向一致。如果索引是 ASC,ORDER BY DESC 可能仍然需要 filesort(MySQL 8.0+ 支持降序索引可以解决这个问题)。
43.9 批量操作优化
-- 差:循环单条插入
INSERT INTO users(username, email) VALUES('user1', 'user1@test.com');
INSERT INTO users(username, email) VALUES('user2', 'user2@test.com');
INSERT INTO users(username, email) VALUES('user3', 'user3@test.com');
-- 每条一个事务,三次网络往返
-- 好:批量插入
INSERT INTO users(username, email) VALUES
('user1', 'user1@test.com'),
('user2', 'user2@test.com'),
('user3', 'user3@test.com');
-- 一个事务,一次网络往返
批量 UPDATE/DELETE 也要分批:
-- 差:一次性删除 100 万条
DELETE FROM logs WHERE created_at < '2025-01-01';
-- 锁多、undo log 大、可能超时
-- 好:分批删除
DELETE FROM logs WHERE created_at < '2025-01-01' LIMIT 1000;
-- 重复执行直到 affected_rows = 0
43.10 SQL 优化速查表
| 优化点 | 差的写法 | 好的写法 |
|---|---|---|
| 查询列 | SELECT * | 只查需要的列 |
| 索引列运算 | WHERE DATE(col) = ... | WHERE col >= ... AND col < ... |
| 隐式转换 | WHERE int_col = '1' | WHERE int_col = 1 |
| LIKE | LIKE '%abc' | LIKE 'abc%' |
| 深分页 | LIMIT 100000, 10 | 延迟关联或记录上次 ID |
| JOIN | 被驱动表无索引 | 给 JOIN 条件列建索引 |
| 批量插入 | 循环单条 INSERT | 一条 INSERT 多值 |
| 批量删除 | 一次删百万行 | 分批 LIMIT 删除 |
| GROUP BY | 无索引 | 给分组列建索引 |
| ORDER BY | 无索引 | 给排序列建索引 |
43.11 小结
- 慢查询日志是发现慢 SQL 的第一手段,
long_query_time设 1 秒比较合理; - 调优流程:EXPLAIN -> 看是否走索引 -> 检查索引失效 -> 优化 SQL 或索引;
SELECT *导致回表和传输浪费,只查需要的列;- JOIN 被驱动表的关联列必须有索引;
- 深分页用延迟关联或记录上次 ID 优化;
- COUNT 大表用估算或汇总表;
- 避免在索引列上用函数和运算;
- GROUP BY 和 ORDER BY 的列建索引可消除临时表和文件排序;
- 批量操作用多值 INSERT 和分批 DELETE/UPDATE。
下一节学用户与权限管理,掌握 MySQL 的账号创建和权限分配。