历史背景
ANSI SQL:2011 标准引入了框架排除子句到窗口函数语法中,解决了窗口框架必然包括当前行的局限性。在此增强之前,开发者不得不采用笨重的自连接或代数操作(从总和中减去当前值)来计算不包括焦点行的聚合。该标准定义了四个排除选项:EXCLUDE NO OTHERS,EXCLUDE CURRENT ROW,EXCLUDE GROUP 和 EXCLUDE TIES,为有序分区内的集合操作提供了确定性的语义。
问题
在分析竞争指标时,例如计算同类产品的平均销售价格时,需要将当前产品排除在平均值之外,查询必须定义一个窗口,涵盖所有相关行,除了当前行。传统的窗口函数,比如 AVG() OVER (PARTITION BY category) 包括当前行,从而导致结果失真。使用子查询或连接实现这一点会增加不必要的复杂性和性能下降,尤其是在处理大型分区数据集时,笛卡尔积或相关子查询的开销可能是无法承受的。
解决方案
在窗口规范中利用框架排除子句:AVG(price) OVER (PARTITION BY category ORDER BY price ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW)。该语法指示 SQL 引擎首先建立完整的分区框架,然后在计算聚合之前逻辑上移除当前行。对于需要排除所有平局值的场景(例如,所有价格相同的产品),EXCLUDE GROUP 移除当前行及其排序的同伴,而 EXCLUDE TIES 保留当前行但移除重复的排序值。
一家电子商务分析团队需要生成“市场位置”报告。对于每个卖方的电子小工具列表,他们必须显示卖方的价格以及所有 其他 卖方提供的相同小工具型号的平均价格。
最初原型设计了一种自连接的方法,其中将列表表与自身在 model_id 上连接,排除匹配的主键。优点:它在所有 SQL 方言中都得到普遍支持,概念上简单直接。缺点:在最坏的情况下,它的执行展现出 O(n²) 的复杂度,导致在数百万行上的指数级下降;此外,查询优化器通常难以处理不平等的连接条件,生成低效的执行计划,导致哈希溢出或嵌套循环连接。
还评估了一种代数解决方案,计算每个型号的全局总和和计数,然后通过 (SUM(price) - current_price) / (COUNT(*) - 1) 导出其他的平均值。优点:避免了连接,并且只需要一次窗口函数扫描。缺点:当 COUNT(*) = 1 时会灾难性失败(导致除以零),或者当价格为 NULL 时需要冗长的 CASE 保护;此外,它不能应用于非代数聚合,例如 MEDIAN 或 MODE。
团队最终选择了 EXCLUDE CURRENT ROW 框架规范。推理:它是声明式的,通过自然返回 NULL 来消除对 NULL 检查 CASE 表达式的需求,并在 O(n) 时间内通过单次排序传播执行,内存开销最小。最终查询将报告生成时间从十二分钟减少到不足十秒。
结果:生产报告每天准确计算 5000 万个列表的竞争基准,对于仅有单一卖家的稀有商品,优雅地显示 NULL (被解释为“没有竞争”),而不是错误或零值。
当使用 RANGE 基于窗口框架与 ROWS 基于窗口框架时,EXCLUDE CURRENT ROW 的行为如何,特别是关于同组?
当窗口框架使用 ROWS 时,EXCLUDE CURRENT ROW 确切地去除一个物理行——当前行——从聚合中。然而,当使用 RANGE(例如 RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING)时,“当前行”在指定范围内概念上代表所有与当前行共享相同排序值的行。在这种情况下,EXCLUDE CURRENT ROW 只会移除特定行实例,保留其他同伴(平局)。相反,EXCLUDE GROUP 无论框架单元如何都会移除当前行和所有同伴,而 EXCLUDE TIES 则移除所有同伴,保留当前行。候选人通常将这些混淆,以为 EXCLUDE CURRENT ROW 与 RANGE 行为类似于 EXCLUDE GROUP,当存在重复排序键时导致不正确的聚合结果。
为什么在单行分区上使用 EXCLUDE CURRENT ROW 的查询会返回 NULL,这与手动减法方法有什么不同?
ANSI SQL 标准定义,针对空集合的聚合返回 NULL。当 EXCLUDE CURRENT ROW 应用于仅包含一行的分区时,框架变为空,从而导致 AVG、SUM 或 COUNT 自动返回 NULL。相比之下,手动方法如 (SUM(col) - col) / (COUNT(*) - 1) 会在算术中遇到除以零或 NULL 传播问题,需要显式的 CASE 语句来安全处理单例分区。候选人经常忽视这种自动的 NULL 处理行为,期望得到零或当前值,而未能认识到 EXCLUDE 为边界条件提供了更好的空安全性。
EXCLUDE 是否可以与任意框架范围结合,比如滑动窗口(例如,ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING),性能影响是什么?
是的,EXCLUDE 子句在任何框架范围内都是有效的,包括 BETWEEN 滑动窗口。例如,AVG(val) OVER (ORDER BY time ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW) 计算两个前值和两个后值的平均值,有效地创建了一个以当前点为中心但排除当前点的四点移动平均值。在性能方面,现代优化器通过流算法与环形缓冲区或双端队列实现此效果,使每个分区保持 O(n) 的复杂度。候选人通常认为 EXCLUDE 需要完全物化分区或仅适用于 UNBOUNDED 框架,错过了它与有界移动窗口的无缝集成,适用于计算中心滚动相关性或稳健平滑等情形,在这些情况下,焦点点不能影响统计数据。