Lógica de evaluación de CASO inesperada

8

Siempre entendí que la CASEdeclaración funcionaba según un principio de "cortocircuito" en que la evaluación de los pasos posteriores no tiene lugar si un paso anterior se evalúa como verdadero. (Esta respuesta ¿La declaración CASE de SQL Server evalúa todas las condiciones o sale en la primera condición VERDADERA? Está relacionada pero no parece cubrir esta situación y se relaciona con SQL Server).

En el siguiente ejemplo, deseo calcular MAX(amount)entre un rango de meses que difiere en función de cuántos meses hay entre las fechas de inicio y de pago.

(Este es obviamente un ejemplo construido, pero la lógica tiene un razonamiento comercial válido en el código real donde veo el problema).

Si hay <5 meses entre las fechas de inicio y de pago , se utilizará la Expresión 1; de lo contrario, se utilizará la Expresión 2 .

Esto produce el error "ORA-01428: el argumento '-1' está fuera de rango" porque 1 registro tiene una condición de datos no válida que da como resultado un valor negativo para el inicio de la cláusula ENTRE de ORDER BY.

Consulta 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

Así que fui por esta segunda consulta para eliminar primero cualquier lugar donde esto pueda ocurrir:

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

Desafortunadamente, hay un comportamiento inesperado que significa que los valores que la Expresión 1 DEBERÍA USAR SERÍAN validados, aunque la declaración no se ejecutará porque la condición negativa ahora está atrapada por el exterior CASE.

Puedo solucionar el problema usando ABSel MONTHS_BETWEENen la Expresión 1 , pero siento que esto debería ser innecesario.

¿Es este comportamiento como se esperaba? Si es así, ¿por qué me parece ilógico y más como un error?


Esto creará una tabla y datos de prueba. La consulta es simplemente yo comprobando que CASEse está tomando la ruta correcta en el .

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
BriteSponge
fuente
3
FWIW SQL Server también tiene sus peculiaridades en esta área donde las cosas no funcionan como se anuncia dba.stackexchange.com/a/12945/3690
Martin Smith
3
En SQL Server, colocar un agregado dentro de una expresión CASE puede forzar la evaluación de partes de la expresión antes de lo esperado . Me pregunto si algo similar está sucediendo aquí.
Aaron Bertrand
Eso suena bastante cerca de esta situación. Me hace preguntarme de qué se trata la lógica para implementar CASE en dos RDBMS diferentes que conduce al mismo tipo de efecto. Interesante.
BriteSponge
1
Me pregunto si esto está permitido (y si muestra el mismo mal comportamiento):MAX(amount) OVER (PARTITION BY ref_no ORDER BY paid_date ASC ROWS BETWEEN GREATEST(0, LEAST(5, MONTHS_BETWEEN(paid_date, start_date))) PRECEDING AND CURRENT ROW)
ypercubeᵀᴹ
@ ypercubeᵀᴹ: la agregación que sugiere no da el error. Tal vez haya un límite de cuán 'profunda' se verá la evaluación. Especulación.
BriteSponge

Respuestas:

2

Así que fue difícil para mí determinar cuál era su pregunta real de la publicación, pero supongo que es eso cuando ejecuta:

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

¿Todavía obtiene ORA-01428: el argumento '-1' está fuera de rango ?

No creo que esto sea un error. Creo que es una cuestión de orden de operación. Oracle necesita hacer el análisis en todas las filas devueltas por el conjunto de resultados. Entonces puede llegar al meollo de la cuestión de transformar la salida.

Un par de formas adicionales de evitar esto sería excluir la fila con una cláusula 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 

O podría insertar un caso en su analítica como:

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

Explicación

Desearía poder encontrar documentación para respaldar el orden de operación, pero no he podido encontrar nada ... todavía.

La CASEevaluación de cortocircuito ocurre después de evaluar la función analítica. El orden de las operaciones para la consulta en cuestión sería:

  1. desde el pago
  2. max over ()
  3. caso.

Entonces, como max over()sucede antes del caso, la consulta falla.

Las funciones analíticas de Oracle se considerarían una fuente de fila . Si ejecuta un plan de explicación en su consulta, debería ver un "orden de ventana" que es el analítico, que genera filas, que le son alimentadas por la fuente de fila anterior, la tabla de pago. Una declaración de caso es una expresión que se evalúa para cada fila en el origen de la fila. Entonces tiene sentido (al menos para mí), que el caso ocurre después de la analítica.

Nick S
fuente
Aprecio las posibles soluciones: siempre es interesante ver cómo otros hacen las cosas. Sin embargo, tengo una manera fácil de evitar esto; La función ABS funciona en mi situación. Además, es posible que esto no sea realmente abrumador, pero si no es así, Oracle debe indicar que la convención general con respecto a la lógica de 'cortocircuito' no se aplica en el caso de las funciones analíticas.
BriteSponge
Esta respuesta tiene soluciones alternativas y una explicación lógica. No creo que las cosas se pongan más definitivas, así que marcaré esto como la respuesta. Gracias
BriteSponge
1

SQL define qué hacer, no cómo hacerlo. Aunque normalmente Oracle hará un cortocircuito en la evaluación de casos, esta es una optimización y, por lo tanto, se evitará si el optimizador cree que una ruta de ejecución diferente proporciona un rendimiento superior. Tal diferencia de optimización se esperaría cuando los análisis están involucrados.

La diferencia de optimización no se limita al caso. Su error puede reproducirse usando la fusión, que normalmente también provocaría un cortocircuito.

select coalesce(1
   , max(1) OVER (partition by ref_no order by paid_date asc 
     rows between months_between(paid_date,start_date) preceding and current row)) 
from payment;

No parece haber ninguna documentación que diga explícitamente que el optimizador pueda ignorar la evaluación de cortocircuito. Lo más cercano (aunque no lo suficientemente cerca) que puedo encontrar es esto :

Todas las declaraciones SQL usan el optimizador, una parte de la base de datos Oracle que determina los medios más eficientes para acceder a los datos especificados.

Esta pregunta muestra que la evaluación de cortocircuito se ignora incluso sin análisis (aunque hay agrupación).

Tom Kyte menciona que los cortocircuitos pueden ignorarse en su respuesta a una pregunta sobre el orden de evaluación de predicados .

Debe abrir un SR con Oracle. Sospecho que lo aceptarán como un error de documentación y mejorarán la documentación en la próxima versión para incluir una advertencia sobre el optimizador.

Leigh Riffel
fuente
Iba a abrir un SR, pero parece que desafortunadamente no podré hacerlo en mi organización.
BriteSponge
-1

Parece que es una ventana lo que hace que Oracle comience a evaluar todas las expresiones en CASE. Ver

create table t (val int);   
insert into t select 0  from dual;  
insert into t select 1  from dual;  
insert into t select -1  from dual;  

select * from t;

select case when val = -1 then 999 else 2/(val + 1) end as res from t;  

select case when val = -1 then 999 else 2/(val + 1 + sum(val) over())  end as res from t;    

select case when val = -1 then 999 else sum(1) over(ORDER BY 1 ROWS BETWEEN val PRECEDING AND CURRENT ROW) end as res from t;    

drop table t;

Las primeras dos consultas funcionan bien.

Serg
fuente