线上 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 字段有索引,但 statuscreated_at 不在复合索引中
  • ORDER BY created_at DESC 触发了 filesort

user_id 索引可以加速等值查询,但后续的 status 条件无法使用索引,created_at 排序也无法利用索引有序特性。

第三步:优化索引结构

根据查询条件设计新的复合索引:

原则:

  1. 等值条件列优先(user_id
  2. 范围或 IN 条件列其次(status
  3. 排序字段加入索引(created_at
  4. 覆盖查询字段加入索引(减少回表)
-- 创建复合索引
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 (...) — 等值 + IN
  • ORDER 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: refrange — 索引扫描
  • rows: 20 或更少
  • Using filesort
  • Using temporary

用 JMeter 或 wrk 压测对比:

# 优化前 QPS: ~35, P99: 30000ms
# 优化后 QPS: ~1200, P99: 180ms

这次优化的核心思路

总结 6 步:

  1. 慢查询日志定位问题 SQL — 先找到是谁慢
  2. EXPLAIN 分析执行计划 — 确定全表扫描、filesort、临时表
  3. 设计合适的复合索引 — 让过滤和排序都能命中索引
  4. 改写 SQL 结构 — 子查询 + 延迟关联减少 JOIN 数据量
  5. 业务层并行化 — 拆复杂查询为多个简单查询
  6. 游标分页替代 OFFSET — 避免深分页问题

避免慢查询的经验

  1. DBA review 所有生产 SQL:新功能上线前 review 索引设计
  2. 慢查询告警:慢查询数量突增要第一时间告警
  3. 定期 EXPLAIN 审查:线上数据分布变化可能导致索引失效
  4. 控制单表数据量:超过 2000 万行的表考虑归档或分表
  5. **避免 SELECT ***:只查需要的字段,减少 IO 和网络传输

那次优化之后,SRE 撤掉了告警,接口 P99 稳定在 200ms 以内。用户下单页面秒级响应,客诉电话变成了表扬电话。

慢查询优化没有银弹,但有套路:找到问题 SQL,分析执行计划,针对性地加索引或改写 SQL,一步步压下去。

最后更新 4/20/2026, 4:48:48 AM