解决慢查询是一个系统化工程 ,不能简单地"头痛医头"。下面提供一个可操作、可执行的完整慢查询解决流程,涵盖了从定位、分析到优化的全过程。
核心解决流程:四步法
1. 定位 → 2. 分析 → 3. 优化 → 4. 验证
第一步:定位慢查询(找到"凶手")
首先要开启慢查询日志,这是最重要的诊断工具。
1. 开启并配置慢查询日志
MySQL示例:
sql
-- 查看当前设置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 临时开启(重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 设置阈值为1秒
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';
-- 永久开启(修改配置文件my.cnf)
slow_query_log = ON
long_query_time = 1
slow_query_log_file = /var/lib/mysql/slow.log
log_queries_not_using_indexes = ON -- 【建议开启】记录未使用索引的查询
min_examined_row_limit = 100 -- 【可选】扫描行数超过此值才记录
2. 使用工具分析慢查询日志
不要直接看原始日志文件,使用分析工具:
| 工具 | 命令示例 | 作用 |
|---|---|---|
| mysqldumpslow (MySQL自带) | mysqldumpslow -s t -t 10 /path/to/slow.log |
按总耗时排序,显示前10条慢查询 |
| pt-query-digest (Percona Toolkit) | pt-query-digest /path/to/slow.log |
更强大的分析,给出优化建议 |
关键分析点:
- 出现次数最多的慢查询
- 总耗时最长的慢查询
- 扫描行数最多的查询
第二步:分析慢查询原因(诊断"病因")
找到具体的慢SQL后,使用 EXPLAIN 或 EXPLAIN ANALYZE 进行深度分析。
1. 使用 EXPLAIN 分析执行计划
sql
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND create_time > '2024-01-01';
重点关注以下几个字段:
| 字段 | 含义 | 问题指示 |
|---|---|---|
| type | 访问类型 | ALL(全表扫描,大问题!)→ 需优化到 range 或 ref |
| key | 实际使用的索引 | 为 NULL 表示没走索引 |
| rows | 预估扫描行数 | 数值越大,查询成本越高 |
| Extra | 额外信息 | Using filesort(文件排序)、Using temporary(临时表)都是危险信号 |
2. 使用 EXPLAIN ANALYZE(MySQL 8.0+ / PostgreSQL)
sql
EXPLAIN ANALYZE SELECT ...;
它会实际执行 查询,并给出各阶段的实际耗时,比 EXPLAIN 更精确。
3. 常见问题模式快速诊断
- 全表扫描 :
type = ALL,通常是缺乏索引 或索引失效。 - 文件排序 :
Extra = Using filesort,ORDER BY没有用到索引。 - 创建临时表 :
Extra = Using temporary,常见于GROUP BY、DISTINCT未用索引。 - 索引使用不当 :
key列显示使用了索引,但rows依然很大,可能是索引选择性差。
第三步:针对性地优化(开出"药方")
针对不同的原因,采取不同的优化策略。按优化效果和成本排序:
1. 索引优化(效果最显著,成本最低)
| 问题现象 | 优化方案 | 示例 |
|---|---|---|
| 没有索引 | 添加最合适的索引 | CREATE INDEX idx_user_id ON orders(user_id); |
| 索引失效 | 改写SQL,避免索引失效 | 避免对索引字段使用函数、计算、类型转换、LIKE '%xx' |
| 排序/分组慢 | 创建支持排序的复合索引 | ORDER BY a, b → 创建 INDEX(a, b) |
| 回表代价高 | 使用覆盖索引 | 查询的字段都包含在索引中:INDEX(user_id, status) 对于 SELECT user_id, status FROM ... |
| 多条件查询 | 创建复合索引,注意顺序 | 高选择性字段放前面,遵循最左前缀原则 |
示例:一个典型的索引优化案例
sql
-- 原慢查询:
SELECT * FROM orders
WHERE user_id = 100
AND status = 'paid'
AND create_time > '2024-01-01'
ORDER BY create_time DESC;
-- 优化:添加复合索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time DESC);
-- 这个索引能同时优化WHERE条件过滤和ORDER BY排序
2. SQL语句优化(不修改表结构)
| 优化策略 | 具体做法 |
|---|---|
| 避免 SELECT * | 只查询需要的字段,减少数据传输和回表 |
| 优化分页查询 | 用 WHERE id > 上次最大ID LIMIT 10 代替 LIMIT 100000, 10 |
| 拆分复杂查询 | 将大查询拆成多个小查询,在应用层组合 |
| 避免使用子查询 | 用 JOIN 改写,但注意JOIN的效率和索引 |
| 使用UNION ALL替代UNION | 如果不需要去重,UNION ALL 效率更高 |
| 合理使用EXISTS/IN | 小表驱动大表时用 IN,大表驱动小表时用 EXISTS |
3. 表结构优化(需要修改结构)
| 优化策略 | 具体做法 |
|---|---|
| 大表拆分 | 垂直拆分(分字段到不同表)、水平拆分(分表) |
| 字段类型优化 | 使用更小的数据类型(如 INT 代替 BIGINT)、NOT NULL 约束 |
| 冗余字段 | 在可接受范围内适当增加冗余字段,避免复杂JOIN |
| 归档历史数据 | 将不活跃的历史数据迁移到归档表 |
4. 架构层面优化(当单实例无法满足时)
| 场景 | 解决方案 |
|---|---|
| 读多写少 | 读写分离,将读请求分发到从库 |
| 写压力大/数据量极大 | 分库分表,按业务维度拆分 |
| 热点数据查询 | 引入缓存(Redis),缓存查询结果 |
| 复杂搜索 | 引入搜索引擎(Elasticsearch) |
| 聚合分析 | 使用列式数据库(ClickHouse) |
第四步:验证优化效果(确保"疗效")
优化后必须验证,否则可能引入新问题。
-
执行计划对比 :再次执行
EXPLAIN,确认:- 访问类型(type)是否改善
- 是否使用了新索引
- 扫描行数(rows)是否减少
- 是否消除了
Using filesort和Using temporary
-
实际执行时间对比:
sql-- 优化前记录时间 SELECT SQL_NO_CACHE ...; -- 多次执行取平均值 -- 优化后记录时间 SELECT SQL_NO_CACHE ...; -- 对比时间 -
监控观察:
- 观察慢查询日志,该SQL是否还出现
- 监控数据库CPU、I/O使用率是否有下降
- 观察应用响应时间是否有改善
实战案例:完整解决一条慢查询
问题SQL:
sql
SELECT * FROM order_items oi
JOIN products p ON oi.product_id = p.id
WHERE oi.order_id IN (
SELECT id FROM orders
WHERE user_id = 100
AND create_time > '2024-01-01'
)
ORDER BY oi.created_at DESC
LIMIT 20;
解决步骤:
-
EXPLAIN分析 :发现对
orders表的子查询进行了全表扫描,order_items的JOIN也没有走索引。 -
优化方案:
sql-- 1. 为orders表添加索引 CREATE INDEX idx_user_time ON orders(user_id, create_time); -- 2. 为order_items表添加索引 CREATE INDEX idx_product_id ON order_items(product_id); CREATE INDEX idx_order_id ON order_items(order_id); -- 3. 改写SQL,用JOIN代替子查询 SELECT oi.* FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id WHERE o.user_id = 100 AND o.create_time > '2024-01-01' ORDER BY oi.created_at DESC LIMIT 20; -- 4. 进一步优化:创建覆盖索引 CREATE INDEX idx_user_time_cover ON orders(user_id, create_time, id); CREATE INDEX idx_order_created ON order_items(order_id, created_at DESC); -
验证效果:
- 再次执行
EXPLAIN,确认全部使用了索引 - 执行时间从原来的 2.3秒 降低到 0.02秒
- 再次执行
总结:慢查询解决工具箱
| 工具/方法 | 用途 |
|---|---|
| 慢查询日志 | 发现哪些查询慢 |
| EXPLAIN / EXPLAIN ANALYZE | 分析为什么慢 |
| 索引优化 | 最有效的优化手段 |
| SQL改写 | 避免低效写法 |
| 性能监控工具 | 实时监控数据库状态 |
| 压力测试 | 验证优化效果 |
记住黄金法则:先诊断,后开方;先索引,后架构;先单机,后分布。 80%的慢查询问题都能通过优化索引和SQL语句解决,只有在数据量真正达到单机瓶颈时,才需要考虑分库分表等复杂架构方案。