← 返回首页

从一次“附属排序”需求,彻底搞懂 SQL 执行顺序与踩坑实录

编程开发 亲身经历 公开
内容简要
在实现主表 zdv10 查询附带 sd140 表字段排序时,通过标量子查询实现无侵入,但遇到 GROUP BY、ORDER BY 执行顺序和子查询执行时机等坑点。
坑点
GROUP BY 时子查询未执行导致报错,ORDER BY 对中间结果集排序而非完整结果集,子查询执行时机和性能问题,以及 ROWNUM 和函数截断问题。
注意事项

1. 确保索引配置合理,包括 zdv10 和 sd140 表的索引。

  • zdv10(zdv10_sys_user, zdv10_month, zdv10_cust_no, zdv10_factory_no)
  • sd140(sd140_factory_no, sd140_seq_no)

2. 使用 JOIN (SELECT MAX... GROUP BY) 替代标量子查询,提高语义清晰度和避免不确定性。

3. 避免使用 ROWNUM,使用 MAX 函数代替。

4. 避免使用 substr 函数导致索引失效,使用 LIKE 代替。

引言:一个“看似简单”的需求

事情的起因很平常:在主表 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 的逻辑执行顺序决定了它的命运:

  1. WHERE:过滤主表
  2. GROUP BY:分组去重(此时子查询还没跑!)
  3. SELECT:计算列(此时才逐组触发子查询,把号取出来)
  4. 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%'。

最终方案与索引保障

经历了这些,我明确了最佳实践:

  1. 索引必须配:
  • zdv10(zdv10_sys_user, zdv10_month, zdv10_cust_no, zdv10_factory_no)(覆盖过滤、分组)
  • sd140(sd140_factory_no, sd140_seq_no)(让子查询只扫索引)
  1. 终极推荐(预聚合 JOIN 版):若想语义更清晰、避免子查询不确定性,可用 JOIN (SELECT MAX... GROUP BY) 替代标量子查询;若追求极致“无侵入”,原标量写法配合索引也可接受。

总结:我的认知升华

这次排查让我牢牢记住三句话:

  • 解耦:用标量或预聚合实现“可插拔”排序,主表更纯粹。
  • 顺序:GROUP BY 不认派生列,ORDER BY 只排最终结果。
  • 性能:先 GROUP BY 压缩行数,再触发子查询,配合索引是王道。

从“写得出”到“写得懂”,这不仅是 SQL 调优的门槛,更是逻辑思维的进阶。

本文内容仅代表作者个人观点、经历或转述,不代表任何官方立场,仅供参考,不构成任何专业建议。据此操作产生的风险由读者自行承担。
发布于 2026-10-28 17:04:38 ID: 27