具有 SUM 聚合的 Postgres CASE 条件评估不需要的 ELSE 部分

Posted

技术标签:

【中文标题】具有 SUM 聚合的 Postgres CASE 条件评估不需要的 ELSE 部分【英文标题】:Posgtres CASE condition with SUM aggregation evaluates not needed ELSE part 【发布时间】:2014-10-10 10:13:16 【问题描述】:

根据Postgres documentation:

CASE 表达式不会计算任何不符合条件的子表达式 需要确定结果。例如,这是一种可能的方式 避免除零失败:

SELECT ... WHERE CASE WHEN 0 THEN y/x > 1.5 ELSE false END;

为什么下面的表达式返回 ERROR: division by zero? - 显然是在评估 else 部分:

SELECT CASE WHEN SUM(0) = 0 THEN 42 ELSE 43 / 0 END

同时

SELECT CASE WHEN SUM(0) = 0 THEN 42 ELSE 43 END

返回 42。

编辑:所以上面的例子失败了,因为 Postgres 已经在计划阶段计算不可变值 (43/0)。我们的实际查询看起来更像这样:

case when sum( column1 ) = 0
            then 0
            else round( sum(   price 
                             * hours 
                             / column1 ), 2 )

虽然这个查询看起来不是一成不变的(取决于实际值),但仍然存在除以零的错误。当然 sum(column1) 在我们的例子中实际上是 0。

【问题讨论】:

您的报价是立即后跟“注意:如第 35.6 节所述,标记为 IMMUTABLE 的函数和运算符可以在计划查询时而不是在执行时进行评估。这意味着在查询执行期间未评估的子表达式的常量部分可能仍会在查询计划期间进行评估。”那么究竟是什么问题呢?有意义的检查类型如上所述,只有没有意义的检查(常量43 / 0 值?)会引发错误。 谢谢,我以前没有读过这个说明。我现在明白为什么我的问题中的简化示例失败了。让我添加一个更接近我们真实代码的示例 - 我不确定文档中的注释是否也解释了这一点.. 实际上,PostgreSQL 跳过了一堆圈子才能使其正常工作。例如,SELECT CASE WHEN TRUE THEN 1 ELSE 1/0 END 成功。您发现 PostgreSQL 延迟零除法评估的正常尝试不起作用,从外观上看,可能与聚合函数有关。我不确定这是否是一个严格的错误(它将取决于 SQL 标准详细信息),但请向 pgsql-bugs postgresql.org/support/submitbug 报告。发布此帖子的链接,但也要像您上面提供的那样提供完整的详细说明。 我将此作为错误报告提交,我们会在了解更多信息后立即更新此问题。 这里是邮件列表存档的链接:postgresql.1045698.n5.nabble.com/… 【参考方案1】:

有趣的例子。这确实有一个很好的解释。假设您有这样的数据:

db=# table test;
 column1 | price | hours 
---------+-------+-------
       1 |     2 |     3
       3 |     2 |     1

PostgreSQL 分两遍执行您的 SELECT,首先它会计算所有存在的聚合函数(如 sum()):

db=# select sum(column1) as sum1, sum(price * hours / column1) as sum2 from test;
 sum1 | sum2 
------+------
    4 |    6

然后它会将这些结果插入您的最终表达式并计算实际结果:

db=# with temp as (
db(#     select sum(column1) as sum1, sum(price * hours / column1) as sum2 from test
db(# ) select case when sum1 = 0 then 0 else round(sum2, 2) end from temp;
 round 
-------
  6.00

现在很明显,如果在第一次聚合过程中出现错误,它永远不会到达 CASE 语句。

所以这在关于 CASE 语句的文档中并不是真正的问题——它适用于所有条件构造——而是关于在 SELECT 语句中处理聚合的方式。这种问题不会在任何其他上下文中发生,因为聚合只允许在 SELECT 中。

但在这种情况下,文档也需要更新。在这种情况下,正确的文档是“the general processing of SELECT”。第 4 步讨论了 GROUP BY 和 HAVING 子句,但它实际上也评估了此步骤中的任何聚合函数,无论 GROUP BY/HAVING 是什么。并且您的 CASE 语句在步骤 #5 中进行评估。

解决方案

常见的解决方案,如果您想忽略否则会导致除以零的聚合输入,请使用 nullif() construct 将它们变成 NULL:

round( sum(   price 
            * hours 
            / nullif(column1, 0) ), 2 )

PostgreSQL 9.4 将为聚合引入一个新的 FILTER 子句,它也可以用于此目的:

round( sum(   price 
            * hours 
            / column1
          ) filter (where column1!=0), 2 )

【讨论】:

好吧,在您提请更多注意这个问题后重新阅读我的答案,我意识到我什至没有找到正确的解决方法。我的方法是可行的,但你实际上已经将你的方法变成了可行的方法。 感谢您的有用解释和更好的解决方案。

以上是关于具有 SUM 聚合的 Postgres CASE 条件评估不需要的 ELSE 部分的主要内容,如果未能解决你的问题,请参考以下文章

Postgres:如何聚合一行以报告具有最高值的列名?

查询分组中的 Oracle SQL 条件聚合函数

SQL LEN() 函数 ,case when,聚合函数的使用方法

如何在 SELECT 查询中使用聚合函数和 CASE WHEN THEN

Mongoose — 使用聚合创建具有 $sum 的新属性

具有 SUM、COUNT 或聚合结果的动态列的 SQL Pivot