引言:一个“看似简单”的需求
事情的起因很平常:在主表 zdv10 的常规查询中,需要附带一个来自 sd140 表的字段(sd140_seq_no),仅仅为了附加排序。我的核心诉求是:绝对不能影响主表查询,不能有额外的数据放大,最好能像开关一样“可加可减”。
正是这个诉求,让我写出了一段“能跑但充满坑”的 SQL,也由此揭开了对 GROUP BY、ORDER BY 以及标量子查询执行顺序的深层理解。
初始探索:如何做到“无侵入”?
最初我尝试用 LEFT JOIN 直接关联 sd140。但很快发现,如果 sd140 一个工厂对应多行,JOIN 会导致 zdv10 数据膨胀,不仅影响统计,还破坏了我“不干扰主表”的底线。
最终,我采用了标量子查询的写法(配合 ROWNUM=1),将 sd140 完全剥离到 SELECT 中:
SELECT zdv10_cust_no,
zdv10_factory_no,
(SELECT b.sd140_seq_no
FROM sd140 b
WHERE b.sd140_factory_no = a.zdv10_factory_no
AND ROWNUM = 1) AS sd140_seq_no
FROM zdv10 a
WHERE zdv10_sys_user = 'KANGKANG_CHEN'
AND substr(zdv10_month, 0, 4) = '2026'
GROUP BY zdv10_cust_no, zdv10_factory_no
ORDER BY zdv10_cust_no DESC, sd140_seq_no;这个写法实现了“无侵入”,但也让我接连踩坑。
踩坑一:为什么放 GROUP BY 报错,放 ORDER BY 却没事?
这是我遇到的最大疑惑:既然 sd140_seq_no 能在最后排序,为什么不能把它放进 GROUP BY?放了直接报 ORA-00904 无效标识符。
理解过程(核心铁律):
SQL 的逻辑执行顺序决定了它的命运:
WHERE:过滤主表GROUP BY:分组去重(此时子查询还没跑!)SELECT:计算列(此时才逐组触发子查询,把号取出来)ORDER BY:最终排序(此时列已算好,现成可用)
结论:GROUP BY 时,sd140_seq_no 还不存在,当然报错;ORDER BY 是最后一步,面对的是已经补齐的完整结果集,所以随便排。这就是“先定规则,后算列,再排队”。
踩坑二:子查询到底什么时候执行?
我曾以为:是先查出所有资料、GROUP BY 全部做完,再去跑子查询。
真实执行节奏:
Oracle 并非完全串行。实际是:先压缩,再逐组补列。
GROUP BY 先把 zdv10 压成 (cust_no, factory_no) 的唯一组合(比如从 50 万行压成 500 行),然后一边产出分组,一边对这 500 组每组触发一次子查询。最后拿到带 sd140_seq_no 的 500 行,交给 ORDER BY 排序。
隐患:如果没索引,这 500 次子查询会成性能杀手(N+1 问题)。
踩坑三:ORDER BY 是查出来再排吗?内存还是磁盘?
理解过程:
是的,ORDER BY 永远是对“中间结果集”(已过滤、已分组、已算列)进行排序。
- 先内存:分组后通常仅数百行,默认在
PGA内存中排序,极快。 - 后磁盘:若结果集巨大,内存装不下,会落盘到临时表空间(性能骤降)。本例因“先压缩”所以大概率纯内存。
踩坑四:隐式坑——ROWNUM 与函数截断
ROWNUM=1在sd140有多条时是不确定取值,严谨做法应是MAX(sd140_seq_no)。substr(zdv10_month, 0, 4)会导致索引失效,应改为zdv10_month LIKE '2026%'。
最终方案与索引保障
经历了这些,我明确了最佳实践:
- 索引必须配:
zdv10(zdv10_sys_user, zdv10_month, zdv10_cust_no, zdv10_factory_no)(覆盖过滤、分组)sd140(sd140_factory_no, sd140_seq_no)(让子查询只扫索引)
- 终极推荐(预聚合 JOIN 版):若想语义更清晰、避免子查询不确定性,可用
JOIN (SELECT MAX... GROUP BY)替代标量子查询;若追求极致“无侵入”,原标量写法配合索引也可接受。
总结:我的认知升华
这次排查让我牢牢记住三句话:
- 解耦:用标量或预聚合实现“可插拔”排序,主表更纯粹。
- 顺序:
GROUP BY不认派生列,ORDER BY只排最终结果。 - 性能:先
GROUP BY压缩行数,再触发子查询,配合索引是王道。
从“写得出”到“写得懂”,这不仅是 SQL 调优的门槛,更是逻辑思维的进阶。