前言
在实际业务开发中,经常需要在主表查询时,顺带统计子表的数据。
如果写法不当,很容易造成 SQL 执行慢、扫描表次数过多 的问题。
本文通过一个真实业务 SQL,讲解如何将 多个相关子查询
优化为 一次 GROUP BY + LEFT JOIN,提升性能和可维护性。
一、业务场景说明
涉及三张表:
- arr_air_waybill:运单主表(1 条运单 1 行)
- hz_arr_record_order:到货记录表(与运单 1 对 1)
- hz_arr_tally:理货记录表(与运单 1 对多)
查询运单列表时,需要返回以下与理货相关的字段:
- 是否已理货
- 理货记录数量
- 理货总件数
- 理货总重量
二、原始 SQL 写法(存在性能问题)
最初的写法是在 SELECT 中对 hz_arr_tally 做多次子查询:
SELECT
aaw.waybill_code,
CASE
WHEN EXISTS (
SELECT 1
FROM hz_arr_tally hat
WHERE hat.waybill_code = aaw.waybill_code
) THEN '是'
ELSE '否'
END AS isTally,
(SELECT COUNT(*)
FROM hz_arr_tally hat2
WHERE hat2.waybill_code = aaw.waybill_code) AS tallyNumber,
(SELECT COUNT(pieces)
FROM hz_arr_tally hat3
WHERE hat3.waybill_code = aaw.waybill_code) AS tallyPieces,
(SELECT COUNT(weight)
FROM hz_arr_tally hat4
WHERE hat4.waybill_code = aaw.waybill_code) AS tallyWeight
FROM arr_air_waybill aaw;
三、存在的问题分析
1️⃣ 重复扫描子表,性能差
- 每一条运单记录
- 都会对
hz_arr_tally执行 4 次查询 - 运单数量一多,性能急剧下降
本质问题:N 行主表 × 多个子查询
2️⃣ COUNT(pieces) / COUNT(weight) 使用不当
COUNT(weight)
统计的是 weight 不为 NULL 的行数,
并不是重量合计。
⚠️ 正确语义应为:
SUM(weight)
SUM(pieces)
四、优化思路
核心原则:
统计类数据不要在 SELECT 中逐行计算,而是先按业务主键 GROUP BY 一次算完,再 JOIN 回主表。
五、优化后的关键 SQL
LEFT JOIN (
SELECT
waybill_code,
COUNT(*) AS tallyNumber,
SUM(pieces) AS tallyPieces,
SUM(weight) AS tallyWeight
FROM hz_arr_tally
GROUP BY waybill_code
) ht
ON ht.waybill_code = aaw.waybill_code
六、SQL 执行逻辑说明(重点)
1️⃣ 子查询:先按运单号汇总理货数据
原始理货表示例:
| waybill_code | pieces | weight |
|---|---|---|
| WB001 | 10 | 100 |
| WB001 | 5 | 40 |
| WB002 | 8 | 60 |
子查询执行结果:
| waybill_code | tallyNumber | tallyPieces | tallyWeight |
|---|---|---|---|
| WB001 | 2 | 15 | 140 |
| WB002 | 1 | 8 | 60 |
👉 一张运单只保留一行汇总数据
2️⃣ LEFT JOIN 回主表
ON ht.waybill_code = aaw.waybill_code
JOIN 后效果:
| waybill_code | consign | tallyNumber | tallyPieces | tallyWeight |
|---|---|---|---|---|
| WB001 | 张三 | 2 | 15 | 140 |
| WB002 | 李四 | 1 | 8 | 60 |
| WB003 | 王五 | NULL | NULL | NULL |
说明:
- 没有理货记录的运单也能正常查询
- 所以必须使用
LEFT JOIN
七、是否理货(isTally)的判断方式
CASE
WHEN ht.waybill_code IS NULL THEN '否'
ELSE '是'
END AS isTally
原理:
- JOIN 成功 → 存在理货记录
- JOIN 失败 → 字段为 NULL
👉 JOIN 结果本身即可判断是否存在数据,无需 EXISTS
八、性能与可维护性对比
| 对比项 | 优化前 | 优化后 |
|---|---|---|
| 子表扫描次数 | 多次 | 1 次 |
| SQL 可读性 | 差 | 高 |
| 统计语义 | 错误风险 | 准确 |
| 扩展新统计字段 | 困难 | 简单 |
九、索引建议
CREATE INDEX idx_tally_waybill
ON hz_arr_tally(waybill_code);
CREATE INDEX idx_air_waybill_flight
ON arr_air_waybill(flight_date1, flight_no1);
十、总结
适合使用 GROUP BY + LEFT JOIN 的场景:
- 主表 + 明细表(1 对多)
- 查询主表时需要统计明细数据
- 明细数据较多,性能要求较高
记住一句话:
统计字段先聚合,再 JOIN;不要在 SELECT 中一行一行算。
如果你想,我还能帮你把下一篇规划成:
- 《MyBatis 中 GROUP BY + LEFT JOIN 的正确写法》
- 《EXISTS / 子查询 / JOIN 性能对比》
- 《MySQL ONLY_FULL_GROUP_BY 实战避坑》
要哪一篇,直接说 👍