一次 MySQL 慢查询优化实战:从索引试错到 SQL 改写

May 22, 2026 min read

一个 WMS 波次计划列表查询一页数据需要约 6 秒。这篇文章记录实际排查路径:从应用日志拿到真实 SQL,检查现有索引,测试并回退无效索引,最后验证新的 SQL 执行结构。

文中的环境和业务标识均已脱敏,SQL 结构、耗时数据和判断过程来自实际排查。

慢查询的执行结构

页面需要查询波次主表,关联明细与出库单,聚合 OMS 订单号和 Homebase,关联创建人、修改人信息,并查询装车时间,最后按创建时间倒序返回 50 条。

日志中的关键筛选条件是修改时间范围、组织和项目条件,同时按照创建时间排序。

问题就在这里:SQL 在确定页面最终需要的 50 个波次前,已经开始展开明细、聚合并排序。

先检查索引,而不是直接加索引

首先核对主表、明细表和出库单表的现有索引。明细关联出库单的路径已经存在可用联合索引。同时,波次主表上存在两条字段顺序完全一致的索引,因此删除其中一条重复索引,减少后续写入维护成本。

这个清理是必要的,但不足以解释 6 秒查询。当前列表按创建时间排序的路径本来就有索引支持,说明瓶颈不只是“没有索引”。

尝试装车时间子查询索引,然后回退

原 SQL 中存在一个相关子查询,逻辑等价于:

SELECT FM_LD_TIME
FROM wm_ld_header
WHERE DEF1 = WWH.WAVE_NO
  AND ORG_ID = WWH.ORG_ID
LIMIT 1;

因为装车表没有按组织和波次引用字段访问的索引,测试增加了 ORG_ID + DEF1(32) 的索引。DEF1TEXT 字段,因此使用前缀索引。

但核查数据分布时发现,生产数据中的 DEF1 全部为空。继续使用 EXPLAIN 验证后,MySQL 虽然识别了该候选索引,实际执行仍然对相关子查询做全表扫描,每次预估扫描一万多行。

因此该测试索引被回退。更重要的结论是:当前数据条件下,装车时间子查询无法返回有效值,却仍在持续增加查询工作量。

按真实过滤字段尝试 MODIFY_TIME 索引,也回退

日志中的范围条件使用 MODIFY_TIME,而当前列表索引主要围绕 CREATE_TIME。因此测试增加了以组织、项目、修改时间、波次号组成的联合索引。

索引的区分度正常,但执行计划仍更倾向使用原有创建时间索引。随后对同一组查询参数进行真实耗时对比:

方案执行路径耗时
A优化器自动选择6.859s
B强制原创建时间索引6.225s
C强制新增修改时间索引7.433s

新增索引在该页面查询下更慢,因此同样删除。这个结果说明:仅继续追加索引,无法解决当前列表的秒级卡顿。

EXPLAIN 给出的证据

原查询执行计划中,明细表与出库单表的关联已经命中索引,单次关联行数很少。真正值得关注的是:

  • 主查询在聚合后需要临时表和文件排序;
  • 装车时间相关子查询作为依赖子查询,扫描超过一万行;
  • 所有这些工作完成后,页面最终只取 50 条。

因此慢点不是单个 JOIN 的缺索引问题,而是执行顺序问题:先聚合大量候选数据,再做分页。

改写方向:先分页主表,再聚合明细

成功的测试查询改变了执行顺序:

  1. 先筛选并排序 wm_wv_header
  2. 只取得页面实际需要的 50 条主表记录;
  3. 只针对这 50 个波次聚合 OMS 订单号和 Homebase;
  4. 在装车时间实际关联关系确认前,当前场景先返回空值,停止无效扫描。

核心结构如下:

SELECT
    WWH.JOB_ID,
    WWH.WAVE_NO,
    (SELECT GROUP_CONCAT(...) FROM wm_wv_detail ... WHERE ... = WWH.WAVE_NO) AS OMS_ORDER_NO,
    (SELECT GROUP_CONCAT(DISTINCT ...) FROM wm_wv_detail ... WHERE ... = WWH.WAVE_NO) AS HOMEBASE_IDS,
    NULL AS FM_LD_TIME
FROM (
    SELECT *
    FROM wm_wv_header
    WHERE MODIFY_TIME BETWEEN :from_time AND :to_time
      AND ORG_ID = :org_id
      AND PROJECT_ID = :project_id
    ORDER BY CREATE_TIME DESC
    LIMIT 50
) WWH
ORDER BY WWH.CREATE_TIME DESC;

这不是说子查询天然比联表快,而是分页被前置之后,明细聚合只处理本页真正需要的数据。

实测结果

使用相同筛选条件并关闭查询缓存后,新结构测试三次:

次数耗时
10.006605s
20.006195s
30.006315s

平均耗时约为 0.00637s。与原查询中较快的一次 6.225s 相比,该测试场景约提升 977 倍,耗时降低约 99.9%。

这个结果只代表本次实际数据与筛选条件下的验证结论,不能直接推断所有查询场景都会获得相同倍率。

上线前的限制:分页不能写死

性能测试将 LIMIT 50 放到了只查询波次主表的内层子查询中,这正是速度提升的关键。

但系统当前由查询框架在外层追加分页。如果在 Hibernate SQL 中直接写死 LIMIT 50,会导致第二页之后无法正确查询,也可能影响导出流程。因此正式实现必须让分页参数进入内层主表查询,或调整分页方式,确保先分页主表、再聚合明细。

正式发布前还需要验证:默认列表、状态筛选、按出库单查询、翻页、导出,以及创建人和修改人筛选。

另一个应单独修复的正确性问题

原查询中,修改人显示关联疑似仍然使用了创建人字段。这个问题会改变展示结果和筛选行为,应作为单独的缺陷修复提交,不应与性能重构混在一个变更中。

总结

这次排查的路径是:

从日志找到真实慢 SQL
→ 检查索引并清理重复项
→ 尝试新增索引并通过实际结果回退
→ 使用 EXPLAIN 与耗时数据定位结构问题
→ 先分页主表,再聚合明细

最终收益不是来自继续堆索引,而是让数据库只对页面真正需要的记录做聚合处理。对于包含一对多明细聚合和分页的列表查询,这通常比继续给复杂 SQL 打补丁更有效。