线上 MySQL 慢查询从 30 秒优化到 200 毫秒,我做了这 6 步
线上 MySQL 慢查询从 30 秒优化到 200 毫秒,我做了这 6 步
凌晨两点,SRE 发来告警:订单查询接口 P99 响应时间超过 30 秒。用户下单后页面一直转圈,客诉电话打爆。
查了慢查询日志,发现问题 SQL 是这样的:
SELECT o.id, o.order_no, o.user_id, o.total_amount, o.status,
u.username, u.mobile, u.email,
p.name AS product_name, p.category,
oi.quantity, oi.price AS item_price
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.user_id = 10086
AND o.status IN (1, 2, 3, 4, 5)
AND o.created_at >= '2025-10-01'
ORDER BY o.created_at DESC
LIMIT 20;
这条 SQL 在测试环境不到 50ms,线上跑了 30 秒。问题在哪?
第一步:确认慢查询并抓取执行计划
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0; -- 抓取所有查询
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 查看慢查询记录
SHOW GLOBAL STATUS LIKE 'Slow_queries';
然后对问题 SQL 做 EXPLAIN:
EXPLAIN SELECT o.id, o.order_no, o.user_id, o.total_amount, o.status,
u.username, u.mobile, u.email,
p.name AS product_name, p.category,
oi.quantity, oi.price AS item_price
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.user_id = 10086
AND o.status IN (1, 2, 3, 4, 5)
AND o.created_at >= '2025-10-01'
ORDER BY o.created_at DESC
LIMIT 20;
执行计划显示:
type: ALL— 全表扫描rows: 986521— 扫了近百万行Using filesort— 内存排序Using temporary— 用了临时表
第二步:分析索引情况
-- 查看表结构
SHOW CREATE TABLE orders;
-- 查看现有索引
SHOW INDEX FROM orders;
发现问题:
user_id字段有索引,但status和created_at不在复合索引中ORDER BY created_at DESC触发了 filesort
user_id 索引可以加速等值查询,但后续的 status 条件无法使用索引,created_at 排序也无法利用索引有序特性。
第三步:优化索引结构
根据查询条件设计新的复合索引:
原则:
- 等值条件列优先(
user_id) - 范围或 IN 条件列其次(
status) - 排序字段加入索引(
created_at) - 覆盖查询字段加入索引(减少回表)
-- 创建复合索引
ALTER TABLE orders DROP INDEX idx_user_id;
ALTER TABLE orders ADD INDEX idx_user_status_created (user_id, status, created_at);
这个索引能覆盖:
WHERE user_id = ?— 等值查询WHERE user_id = ? AND status IN (...)— 等值 + INORDER BY user_id, status, created_at— 排序
第四步:优化 SQL 本身
拆解 JOIN,减少数据量
原 SQL JOIN 了 order_items,但很多订单只有一条明细,JOIN 后数据膨胀。
改用子查询先过滤订单:
SELECT o.id, o.order_no, o.user_id, o.total_amount, o.status,
u.username, u.mobile, u.email,
p.name AS product_name, p.category
FROM (
SELECT id, order_no, user_id, total_amount, status, created_at
FROM orders
WHERE user_id = 10086
AND status IN (1, 2, 3, 4, 5)
AND created_at >= '2025-10-01'
ORDER BY created_at DESC
LIMIT 20
) o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;
内层子查询只取 20 条订单,外层 JOIN 最多 20 条,IO 大幅减少。
延迟关联
如果子查询无法利用索引,可以考虑延迟关联:
SELECT o.id, o.order_no, o.user_id, o.total_amount, o.status,
u.username, u.mobile, u.email,
p.name AS product_name, p.category,
oi.quantity, oi.price AS item_price
FROM (
SELECT id FROM orders
WHERE user_id = 10086
AND status IN (1, 2, 3, 4, 5)
AND created_at >= '2025-10-01'
ORDER BY created_at DESC
LIMIT 20
) t
JOIN orders o ON t.id = o.id
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
LEFT JOIN order_items oi ON o.id = oi.order_id;
业务层并行查询
如果一定要 JOIN 多个表,可以在应用层并行发起多个简单查询:
// 伪代码
List<Long> orderIds = jdbc.queryForList(
"SELECT id FROM orders WHERE user_id = ? AND status IN (1,2,3,4,5) AND created_at >= ? ORDER BY created_at DESC LIMIT 20",
Long.class, userId, startDate);
List<Order> orders = jdbc.queryForList(
"SELECT * FROM orders WHERE id IN (?)", Order.class, orderIds);
Map<Long, User> users = jdbc.queryForList(
"SELECT * FROM users WHERE id IN (?)", User.class, orderIds.stream().map(Order::getUserId).toList())
.stream().collect(toMap(User::getId, u -> u));
业务层组装数据,避免数据库做复杂的 JOIN 和排序。
第五步:分页优化
如果用户要翻页,传统 LIMIT 1000, 20 会跳过前 1000 行再取 20 行,非常慢。
方案一:游标分页
-- 第一页
SELECT * FROM orders
WHERE user_id = 10086 AND created_at < '2026-02-06 00:00:00'
ORDER BY created_at DESC
LIMIT 20;
-- 下一页,传入上一页最后一条的 created_at
SELECT * FROM orders
WHERE user_id = 10086 AND created_at < '2026-02-05 15:30:00'
ORDER BY created_at DESC
LIMIT 20;
方案二:延迟关联 + 上一页最大 ID
-- 第一页
SELECT * FROM orders WHERE user_id = 10086 ORDER BY id DESC LIMIT 20;
-- 下一页
SELECT * FROM orders
WHERE user_id = 10086 AND id < #{lastId}
ORDER BY id DESC LIMIT 20;
前提是 ID 有单调性。
第六步:验证优化效果
改完上线前,在测试环境用 EXPLAIN 验证:
EXPLAIN SELECT o.id, o.order_no, o.user_id, o.total_amount, o.status,
u.username, u.mobile, u.email,
p.name AS product_name, p.category
FROM (
SELECT id, order_no, user_id, total_amount, status, created_at
FROM orders
WHERE user_id = 10086
AND status IN (1, 2, 3, 4, 5)
AND created_at >= '2025-10-01'
ORDER BY created_at DESC
LIMIT 20
) o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;
执行计划应该变成:
type: ref或range— 索引扫描rows: 20或更少- 无
Using filesort - 无
Using temporary
用 JMeter 或 wrk 压测对比:
# 优化前 QPS: ~35, P99: 30000ms
# 优化后 QPS: ~1200, P99: 180ms
这次优化的核心思路
总结 6 步:
- 慢查询日志定位问题 SQL — 先找到是谁慢
- EXPLAIN 分析执行计划 — 确定全表扫描、filesort、临时表
- 设计合适的复合索引 — 让过滤和排序都能命中索引
- 改写 SQL 结构 — 子查询 + 延迟关联减少 JOIN 数据量
- 业务层并行化 — 拆复杂查询为多个简单查询
- 游标分页替代 OFFSET — 避免深分页问题
避免慢查询的经验
- DBA review 所有生产 SQL:新功能上线前 review 索引设计
- 慢查询告警:慢查询数量突增要第一时间告警
- 定期
EXPLAIN审查:线上数据分布变化可能导致索引失效 - 控制单表数据量:超过 2000 万行的表考虑归档或分表
- **避免 SELECT ***:只查需要的字段,减少 IO 和网络传输
那次优化之后,SRE 撤掉了告警,接口 P99 稳定在 200ms 以内。用户下单页面秒级响应,客诉电话变成了表扬电话。
慢查询优化没有银弹,但有套路:找到问题 SQL,分析执行计划,针对性地加索引或改写 SQL,一步步压下去。
