意外的 CASE 评估逻辑

Bri*_*nge 8 oracle

我一直都明白,该CASE语句遵循“短路”原则,因为如果先前步骤被评估为真,则不会对后续步骤进行评估。(此答案是否 SQL Server CASE 语句评估所有条件或在第一个 TRUE 条件时退出?相关但似乎并未涵盖这种情况并且与 SQL Server 相关)。

在以下示例中,我希望MAX(amount)根据开始日期和支付日期之间的月份数计算不同月份之间的月份范围。

(这显然是一个构建的示例,但该逻辑在我看到问题的实际代码中具有有效的业务推理)。

如果开始日期和支付日期之间的时间小于 5 个月,则将使用表达式 1,否则将使用表达式 2

这会导致错误“ORA-01428:参数‘-1’超出范围”,因为 1 条记录的数据条件无效,导致 ORDER BY 的 BETWEEN 子句的开头为负值。

查询 1

SELECT ref_no,
       CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
-- Expression 1
          MAX(amount)
             OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
             ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING
             AND CURRENT ROW)
       ELSE
-- Expression 2
           MAX(amount)
             OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
             ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
       END                
    END 
  FROM payment
Run Code Online (Sandbox Code Playgroud)

所以我进行了第二个查询,首先消除任何可能发生的地方:

SELECT ref_no,
       CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 THEN 0
       ELSE
          CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
             MAX(amount)
                OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
                ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING 
                AND CURRENT ROW)
          ELSE
             MAX(amount)
                OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
                ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
          END                
       END
  FROM payment
Run Code Online (Sandbox Code Playgroud)

不幸的是,有一些意外行为意味着表达式 1使用的值会被验证,即使语句不会被执行,因为否定条件现在被外部CASE.

我可以通过在表达式 1ABS上使用来解决这个问题,但我觉得这应该是不必要的。MONTHS_BETWEEN

这种行为是否符合预期?如果是这样,“为什么”对我来说似乎不合逻辑,更像是一个错误?


这将创建一个表和测试数据。查询只是我检查CASE是否正在采用正确的路径。

CREATE TABLE payment
(ref_no NUMBER,
 start_date DATE,
 paid_date  DATE,
 amount  NUMBER)
 
INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('01-01-2016','DD-MM-YYYY'),3000)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('12-12-2015','DD-MM-YYYY'),5000)

INSERT INTO payment
VALUES (1001,TO_DATE('10-03-2016','DD-MM-YYYY'),TO_DATE('10-02-2016','DD-MM-YYYY'),2000)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('03-03-2016','DD-MM-YYYY'),6000)

INSERT INTO payment
VALUES (1001,TO_DATE('01-11-2015','DD-MM-YYYY'),TO_DATE('28-11-2015','DD-MM-YYYY'),10000)

SELECT ref_no,
       CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 THEN '<0'
       ELSE
          CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
             '<5'
         --    MAX(amount)
         --       OVER (PARTITION BY ref_no ORDER BY paid_date ASC ROWS
         --       BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING
         --       AND CURRENT ROW)
          ELSE
             '>=5'
         --    MAX(amount)
         --       OVER (PARTITION BY ref_no ORDER BY paid_date ASC ROWS
         --       BETWEEN 5 PRECEDING AND CURRENT ROW)
          END                
       END
  FROM payment
Run Code Online (Sandbox Code Playgroud)

小智 2

因此,我很难从帖子中确定您的实际问题是什么,但我认为当您执行时:

SELECT ref_no,
   CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 THEN 0
   ELSE
      CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
         MAX(amount)
            OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
            ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING 
            AND CURRENT ROW)
      ELSE
         MAX(amount)
            OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
            ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
      END                
   END
FROM payment
Run Code Online (Sandbox Code Playgroud)

您仍然收到ORA-01428: argument '-1' is out of range 吗

我不认为这是一个错误。我认为这是一个操作顺序的问题。Oracle 需要对结果集返回的所有行进行分析。然后就可以深入了解转换输出的实质内容。

解决这个问题的一些其他方法是使用 where 子句排除该行:

SELECT ref_no,
   CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
   -- Expression 1
      MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
         ROWS BETWEEN MONTHS_BETWEEN(paid_date, start_date) PRECEDING
         AND CURRENT ROW)
   ELSE
   -- Expression 2
       MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
         ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
   END                
END 
FROM payment
-- this excludes the row from being processed
where MONTHS_BETWEEN(paid_date, start_date) > 0 
Run Code Online (Sandbox Code Playgroud)

或者您可以在分析中嵌入一个案例,例如:

SELECT ref_no,
   CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 5 THEN
-- Expression 1
      MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
               ROWS BETWEEN 
               -- This case will be evaluated when the analytic is evaluated
               CASE WHEN MONTHS_BETWEEN(paid_date, start_date) < 0 
                THEN 0 
                ELSE MONTHS_BETWEEN(paid_date, start_date) 
                END 
              PRECEDING
              AND CURRENT ROW)
   ELSE
-- Expression 2
       MAX(amount)
         OVER (PARTITION BY ref_no ORDER BY paid_date ASC 
         ROWS BETWEEN 5 PRECEDING AND CURRENT ROW)
   END                
END 
FROM payment
Run Code Online (Sandbox Code Playgroud)

解释

我希望我能找到一些文档来支持操作顺序,但我还没有找到任何东西......

短路CASE评估发生在评估解析函数之后。相关查询的操作顺序为:

  1. 从付款
  2. 最大超过()
  3. 案件。

因此,由于该max over()情况发生在该情况之前,因此查询失败。

Oracle 的分析函数将被视为行源。如果您对查询执行解释计划,您应该看到一个“窗口排序”,它是分析生成的行,这些行由前一个行源(付款表)提供给它。case 语句是针对行源中的每一行进行计算的表达式。因此,这种情况发生在分析之后是有道理的(至少对我来说)。