案例 一条四表 JOIN 慢 SQL 的完整复盘:从 8 秒压到 0.03 秒

2026-08-29 21:08:29 views 6

最近排查了一个老项目的订单列表接口,打开页面要等八九秒,用户投诉好几次了。翻了慢查询日志,很快就定位到一条 SQL。

SELECT * FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
ORDER BY o.created_at DESC
LIMIT 20;

慢查询日志记录:

  • Query_time: 8.215682s
  • Rows_sent: 20
  • Rows_examined: 1847234

扫描了 184 万行,只返回 20 行,无效扫描比接近 92361:1。这个比例基本说明 SQL 没有走任何有效的索引路径。

EXPLAIN 里三个致命信号

EXPLAIN 结果里有三个典型的性能信号:

字段含义
typeALLorders 表全表扫描
ExtraUsing filesortORDER BY created_at 没有走索引排序,额外排序
ExtraUsing join buffer关联字段无索引,MySQL 用 join buffer 缓存中间结果

其中 Using join buffer 意味着 JOIN 过程根本没有可用索引,只能把驱动表结果集缓存起来,再去被驱动表逐行匹配。加上 Using filesort,184 万行全部扫完后还要再做一次排序,性能直接崩。

修复过程

1. orders 表建联合索引

最核心的一条:

ALTER TABLE orders ADD INDEX idx_status_created_at (status, created_at);

为什么要这样建?WHERE 条件是 status = 'paid' 等值过滤,ORDER BY 是 created_at DESC。基于最左前缀原则,把等值条件的 status 放前面,created_at 放后面,这样索引既能过滤 status,也能直接按 created_at 有序输出,filesort 自然消失。

如果把 created_at 放前面建 (created_at, status),那 status = 'paid' 这个等值条件走不上索引前缀,联合索引退化成无效索引,完全没意义。

2. order_items JOIN 列补索引

ALTER TABLE order_items ADD INDEX idx_order_id (order_id);
ALTER TABLE order_items ADD INDEX idx_product_id (product_id);

order_items 是被驱动表,JOIN 时靠 order_idproduct_id 关联。这两个字段没索引,MySQL 只能反复全表扫描,Using join buffer 就是从这里来的。

3. 查询字段收窄,减少回表

原 SQL 用了 SELECT *,但订单列表页实际上根本用不着所有列。改成只查需要的字段:

SELECT
  o.id,
  o.order_no,
  o.total_amount,
  o.created_at,
  o.status,
  u.nickname,
  u.avatar,
  COUNT(oi.id)
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY o.id, o.order_no, o.total_amount, o.created_at, o.status, u.nickname, u.avatar
ORDER BY o.created_at DESC
LIMIT 20;

这样做的目的:

  • 避免 SELECT * 回表取全量字段,只取列表页渲染需要的列;
  • COUNT(oi.id) 用于展示订单商品数量,避免在应用层二次查询;
  • 去掉 products 表 JOIN,因为列表页不需要商品详情,这个 JOIN 纯属多余。

效果对比

指标优化前优化后提升倍数
查询耗时8.215s0.031s约 265 倍
扫描行数1847234287约 6436 倍

EXPLAIN 复查,原来的 Using filesortUsing join buffer 均已消除,orders 的 type 从 ALL 变为 ref,order_items 的关联也走上了索引。

一点复盘

这个问题的根因有三个层次:主表缺过滤+排序的联合索引、JOIN 表缺关联索引、SELECT * 放大了回表代价。修的时候按这个顺序来,效果是层层叠加的。

另外,ESLint 之类的工具救不了这种问题。最直接的还是对业务 SQL 保持敏感:一旦 Rows_examinedRows_sent 差距过大,基本就是访问路径出问题了,第一时间 EXPLAIN。

复制全文 生成海报 MySQL 性能 优化

推荐文章

程序员茄子在线接单