跳到主要内容
弗美科技
技术分享

MySQL 索引优化,3 张表 join 从 4 秒到 0.3 秒

一个跑了五年的报表查询,3 张表 JOIN,月底一次要 4 秒多,多人同时点就排队。没分库没上缓存,先拿 EXPLAIN 摊开执行计划:一张表没走索引、JOIN 顺序不对、ORDER BY 触发了 filesort。补两个联合索引、改驱动表、把排序字段并进索引,最后 0.3 秒。

2025年9月19日数据库索引优化慢查询

先说清楚:这不是线上接口,是后台报表,月底财务点一次。跑了五年,从 1 秒多变成 4 秒多,两三个人同时点就转圈。

数据量不大:orders 12 万行,order_items 40 多万行,customers 8000 多行。三张表 JOIN,按月份和客户筛,按金额排序取前 200 行。

第一件事还是看 EXPLAIN

2016 年那次慢查询我们就是靠 EXPLAIN 弄明白的,这次一样,先把语句贴进去看执行计划。

```
EXPLAIN SELECT ... FROM orders o JOIN order_items i ON ... JOIN customers c ON ... WHERE o.month = '2025-08' AND c.region = '华北' ORDER BY o.amount DESC LIMIT 200
```

输出三行,问题全摆着:

  • customers 那行 type 是 ALL,rows 8000 多,没走索引。region 字段上压根没建索引。
  • 优化器把 order_items 当成了驱动表,40 多万行的表放在最外层,后面两张跟着被拖着跑。
  • 最后一行 Extra 里写着 Using filesort; Using temporary。排序没吃上索引,MySQL 自己开了临时表排。

四秒多就这么来的:全表扫客户、大表当起点、结果再搬去排序。

1. 建索引,但别乱建

第一反应是给 region 加个索引。加了,从 4 秒多到 3 秒出头,还是慢。

真正要建的是联合索引,顺序得按查询怎么用来定:

  • customers 上建 (region, id)。region 用来筛,id 是 JOIN 键,放后面能让回表少一点。
  • orders 上原来只有 month 单列索引,改成 (month, customer_id, amount)。month 筛、customer_id 关联、amount 参与排序,三列按这个顺序排,正好覆盖 WHERE、JOIN、ORDER BY。
  • order_items 上已有 (order_id) 索引,够用,没动。

这里有个先后:先想清楚「这条 SQL 按什么顺序找数据」,再决定索引列顺序。反着来,建一堆单列索引,MySQL 多半只挑一个用,剩下的白占空间。

建完再看 EXPLAIN,customers 那行从 ALL 变成 ref,rows 从 8000 多掉到 600 多。

2. 驱动表不让优化器猜了

40 多万行那张被选成驱动表,多半是统计信息旧了。跑了 ANALYZE TABLE 好一点,但月底数据一导入又会飘。

最后用了 STRAIGHT_JOIN,把 JOIN 顺序写死:先 customers 按 region 筛出几百行,再拿这些 id 去 orders 里按 (month, customer_id) 找,最后才关联明细。小结果集在前面,后面每一步都跟着变小。

这么写有代价:数据分布变了,写死的顺序可能不再最优。注释里写了「按华北占比约 7% 定的顺序」,留了开关能退回普通 JOIN。

3. 干掉 filesort

ORDER BY o.amount DESC 跟 WHERE 里的 month 不是同一列,MySQL 得单独排。

把 amount 并进 orders 那个联合索引的最后一列之后,(month, customer_id, amount) 里 amount 本身有序。但还有个坎:customer_id 是范围条件,IN 一串 id,按最左前缀,范围之后的列不能用来排序,filesort 还在。

后来改了写法:不在外层 ORDER BY,改成先按客户分组取各自的前 N 条,再在应用层合并排序。每个客户的查询都能吃满 (month, customer_id, amount) 索引,排序在索引里就完成了。应用层合并 200 行,几乎不花时间。

EXPLAIN 里 Using filesort 和 Using temporary 都消失了。

中间走过的弯路

不是一路顺的。给 orders 加过 (amount) 单列索引,想着排序快些,结果写入慢了 15%,查询没快,优化器根本没选它,回滚删掉。还试过把 LIMIT 200 挪进子查询,行数不确定,取少了,报表数字对不上,财务当天就发现。两处都记在文档里,标题写「别干的事」。

结果

四个动作做完,同一条 SQL 从 4 秒多到 0.3 秒出头,月底三个人同时点也不排队了。没分库、没加 Redis、没升配,就是两个联合索引、一行 STRAIGHT_JOIN、一处改写。

慢查询阈值顺手从 1 秒调到 0.5 秒,想早点看见下一条变慢的语句。

一句大白话

五年里没人改过这条 SQL,是数据自己长胖的。索引不是上线时建好就完事的东西,数据量翻几倍之后,得再拿 EXPLAIN 看一眼。这次要是早点看,不至于等财务来问「怎么又转圈了」。

9 月 19 号下午改完,把那张 EXPLAIN 截图贴进内部文档《两小时》旁边。两张隔着九年,毛病一样:以为以前配过就不用再看了。

商务咨询 · 免费初步沟通

需要针对你的场景给个建议?

把业务背景和当前系统情况告诉我们,我们会给出可执行的思路。

在线留言

周一至周五 9:00–18:00 · 工作日 9:00–18:00 响应,留言会在一个工作日内回复。

商务微信
商务微信二维码

扫一扫,加商务微信咨询服务与方案报价

或填写需求表单