一条后台订单列表,点开要等半天。
表面上看像是前端慢,或者 MySQL 卡了。实际跑了一圈 EXPLAIN ANALYZE 才发现,真正拖慢链路的不是单一索引,而是四表联查里主表没筛住、关联表跟着陪跑。
这篇不讲索引课,讲一次真实的工程化优化:索引和 SQL 一起改。
一、先把问题说清楚:为什么这条 SQL 会慢
原始查询是一个很典型的订单列表页:
SELECT
o.id,
o.order_no,
o.status,
o.amount,
o.create_time,
u.nickname,
u.phone,
m.merchant_name,
p.pay_status,
l.delivery_status
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN merchants m ON m.id = o.merchant_id
LEFT JOIN order_payments p ON p.order_id = o.id
LEFT JOIN order_logistics l ON l.order_id = o.id
WHERE o.merchant_id = 10086
AND o.status IN (1, 2, 3)
AND o.create_time >= '2026-01-01'
AND u.region_id IN (1, 3, 5)
AND m.status = 1
ORDER BY o.create_time DESC
LIMIT 20;
这个场景很真实:
如果主表没吃到索引,后面的 join 再漂亮也救不回来。
二、先看执行计划:瓶颈其实很直接
优化前的 EXPLAIN ANALYZE 里,最重的一句是:
Table scan on o
rows = 1e+6
actual time = 21794ms
也就是说,orders 先扫了 100 万行。
后面还有排序:
Sort: o.create_time DESC
支付表和物流表也各自扫了 30 万行左右。
整条链路跑下来,大概到了 38 秒级别。
这时候不要先动一堆表,先盯最重的那一环:orders。
三、主表先吃索引,关联表只补关键索引
我先给主表加联合索引:
CREATE INDEX idx_orders_merchant_status_ct_user
ON orders(merchant_id, status, create_time, user_id);
这个顺序不是拍脑袋:
一句话:等值字段放前面,范围字段放后面。
关联表只补真正会被用到的键,不乱建:
CREATE INDEX idx_users_region_id_id ON users(region_id, id);
CREATE INDEX idx_order_payments_order_id ON order_payments(order_id);
CREATE INDEX idx_order_logistics_order_id ON order_logistics(order_id);
如果 merchants 也经常按状态筛选,再补:
CREATE INDEX idx_merchants_status_id ON merchants(status, id);
这里要记住一件事:索引不是越多越快。 它会增加写入成本,也会增加维护成本。
四、索引之外,还要改 SQL
这次优化里,第二件事更关键:不是只加索引,而是把 SQL 改得更省。
很多慢查询的问题,不是“找得慢”,而是“找得太多”。
所以我把主查询改成先截断,再 join:
SELECT
o.id,
o.order_no,
o.status,
o.amount,
o.create_time,
u.nickname,
u.phone,
m.merchant_name,
p.pay_status,
l.delivery_status
FROM (
SELECT id, user_id, merchant_id, status, amount, create_time
FROM orders
WHERE merchant_id = 10086
AND status IN (1, 2, 3)
AND create_time >= '2026-01-01'
ORDER BY create_time DESC
LIMIT 20
) o
JOIN users u ON u.id = o.user_id
JOIN merchants m ON m.id = o.merchant_id
LEFT JOIN order_payments p ON p.order_id = o.id
LEFT JOIN order_logistics l ON l.order_id = o.id
WHERE u.region_id IN (1, 3, 5)
AND m.status = 1
ORDER BY o.create_time DESC;
这个思路很重要:
先缩小主表结果集,再去连其他表
而不是把四张表先搅成一锅,再让数据库慢慢筛。
五、优化后的结果:两类场景都验证了
1. 列表联查
优化后,orders 已经走索引了:
Index range scan on o using idx_orders_merchant_status_ct_user
主表不再全表扫,耗时直接降到毫秒级。
不过这里有个细节要实话实说:
最终返回 rows = 0
原因是 u.region_id IN (1, 3, 5) 把前面筛出来的候选行又过滤掉了。
这不影响优化结论,只是说明这组测试条件下没有命中最终结果。
2. 订单详情
我又测了一个非常典型的详情查询:
SELECT
o.id, o.order_no, o.status, o.amount, o.create_time,
u.nickname, u.phone
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.order_no = 'O000000000000001';
优化前:
Table scan on o
actual time = 21728ms
优化后,补了唯一索引:
CREATE UNIQUE INDEX idx_orders_order_no ON orders(order_no);
再跑一次:
Rows fetched before execution
actual time = 0.0127..0.019
这个对比非常适合写进文章里,因为它说明:
唯一业务键,必须单独处理
六、分页也不能只靠索引硬扛
我还测了一个深分页场景:
SELECT
o.id, o.order_no, o.status, o.create_time
FROM orders o
WHERE o.merchant_id = 10086
AND o.status IN (1, 2, 3)
ORDER BY o.create_time DESC
LIMIT 100000, 20;
这种写法很多后台都在用,但它的问题是:
越往后翻,越慢
更好的方式是游标分页:
SELECT
o.id, o.order_no, o.status, o.create_time
FROM orders o
WHERE o.merchant_id = 10086
AND o.status IN (1, 2, 3)
AND o.create_time < '上一页最后一条时间'
ORDER BY o.create_time DESC
LIMIT 20;
这不是加索引,而是改查询方式。
七、这次实战的决策表
八、最后的结论
这次优化最有价值的地方,不是“加了几个索引”,而是确认了一条完整路径:
索引负责缩小范围,SQL 负责控制过程
真正的优化,从来不是单点动作,而是索引和 SQL 一起改。
如果你下次也遇到四表联查慢,先别急着加一堆索引。先看主表有没有被筛住,再看 join 是否真的必要,最后再看分页方式有没有拖后腿。
阅读原文:点击这里
该文章在 2026/8/24 14:43:16 编辑过