Lógica de avaliação CASE inesperada

8

Eu sempre entendi que a CASEdeclaração trabalhava em um princípio de "curto-circuito", pois a avaliação das etapas subsequentes não ocorre se uma etapa anterior for avaliada como verdadeira. (Esta resposta A instrução CASE do SQL Server avalia todas as condições ou sai na primeira condição VERDADEIRA? Está relacionada, mas parece não cobrir essa situação e está relacionada ao SQL Server).

No exemplo a seguir, desejo calcular o MAX(amount)intervalo de meses que difere com base em quantos meses estão entre as datas de início e de pagamento.

(Este é obviamente um exemplo construído, mas a lógica tem um raciocínio comercial válido no código real em que vejo o problema).

Se houver menos de 5 meses entre as datas de início e de pagamento, a Expressão 1 será usada, caso contrário, a Expressão 2 será usada.

Isso resulta no erro "ORA-01428: argumento '-1' está fora do intervalo" porque 1 registro possui uma condição de dados inválida que resulta em um valor negativo para o início da cláusula BETWEEN da 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

Então, eu fui para esta segunda consulta para eliminar primeiro em qualquer lugar que isso possa ocorrer:

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

Infelizmente, há algum comportamento inesperado que significa que os valores usados ​​pela Expressão 1 são validados, mesmo que a instrução não seja executada porque a condição negativa agora está interceptada pelo externo CASE.

I pode contornar o problema usando ABSno MONTHS_BETWEENno Expression 1 , mas eu sinto que este deve ser desnecessário.

Esse comportamento é o esperado? Se sim, 'por que', como parece ilógico para mim e mais como um bug?


Isso criará uma tabela e dados de teste. A consulta é simplesmente eu verificando se o caminho correto CASEestá sendo usado.

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
fonte
3
FWIW SQL Server também tem suas peculiaridades nesta área onde as coisas não chegam a trabalhar como anunciado dba.stackexchange.com/a/12945/3690
Martin Smith
3
No SQL Server, a inserção de um agregado em uma expressão CASE pode forçar partes da expressão a serem avaliadas antes do esperado . Gostaria de saber se algo semelhante está acontecendo aqui?
Aaron Bertrand
Isso soa bem perto dessa situação. Me faz pensar o que é a lógica para implementar o CASE em dois RDBMS diferentes que levam ao mesmo tipo de efeito. Interessante.
BriteSponge
11
Gostaria de saber se isso é permitido (e se mostra o mesmo mau comportamento):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ᵀᴹ: A agregação que você sugere não fornece o erro. Talvez haja um limite para a profundidade da avaliação. Especulação.
BriteSponge

Respostas:

2

Portanto, foi difícil para mim determinar qual era sua pergunta real no post, mas presumo que seja quando você executa:

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

Você ainda recebe ORA-01428: o argumento '-1' está fora do intervalo ?

Eu não acho que isso seja um bug. Eu acho que é uma coisa de ordem de operação. A Oracle precisa fazer a análise em todas as linhas retornadas pelo conjunto de resultados. Então, pode-se chegar ao âmago da questão de transformar a saída.

Algumas maneiras adicionais de contornar isso seriam excluir a linha com uma 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 

Ou você pode incorporar um caso em sua análise, 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

Explicação

Gostaria de encontrar alguma documentação para fazer backup da ordem de operação, mas ainda não consegui encontrar nada ...

A CASEavaliação de curto-circuito ocorre após a avaliação da função analítica. A ordem das operações para a consulta em questão seria:

  1. do pagamento
  2. max over ()
  3. caso.

Portanto, como max over()acontece antes do caso, a consulta falha.

As funções analíticas da Oracle seriam consideradas uma fonte de linha . Se você executar um plano de explicação em sua consulta, verá uma "classificação de janela" que é a analítica, gerando linhas, que são alimentadas pela fonte de linha anterior, a tabela de pagamento. Uma instrução de caso é uma expressão que é avaliada para cada linha na origem da linha. Portanto, faz sentido (pelo menos para mim) que o caso ocorra após a análise.

Nick S
fonte
Agradeço o trabalho potencial - sempre é interessante ver como os outros fazem as coisas. No entanto, tenho uma maneira fácil de contornar isso; a função ABS funciona na minha situação. Além disso, é possível que isso não seja realmente satisfatório, mas caso contrário, a Oracle precisará declarar que a ampla convenção sobre lógica de 'curto-circuito' não se aplica no caso de funções analíticas.
BriteSponge
Esta resposta tem soluções alternativas e uma explicação lógica. Eu não acho que as coisas ficarão mais definitivas e por isso vou marcar isso como a resposta. Obrigado
BriteSponge
1

SQL define o que fazer, não como fazê-lo. Embora normalmente a Oracle faça um curto-circuito na avaliação de caso, essa é uma otimização e, portanto, será evitada se o otimizador acreditar que um caminho de execução diferente fornece desempenho superior. Essa diferença de otimização seria esperada quando a análise estiver envolvida.

A diferença de otimização não se limita ao caso. Seu erro pode ser reproduzido usando coalescência, o que normalmente também causa um curto-circuito.

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;

Parece não haver nenhuma documentação explicitamente dizendo que a avaliação de curto-circuito pode ser ignorada pelo otimizador. A coisa mais próxima (embora não suficientemente próxima) que eu possa encontrar é esta :

Todas as instruções SQL usam o otimizador, uma parte do banco de dados Oracle que determina os meios mais eficientes de acessar os dados especificados.

Essa pergunta mostra a avaliação de curto-circuito sendo ignorada mesmo sem análises (embora exista um agrupamento).

Tom Kyte menciona que o curto-circuito pode ser ignorado em sua resposta a uma pergunta da ordem de avaliação de predicados .

Você deve abrir um SR com Oracle. Suspeito que eles o aceitem como um erro de documentação e aprimore a documentação na próxima versão para incluir uma ressalva sobre o otimizador.

Leigh Riffel
fonte
Ia abrir um SR, mas parece que não poderei fazer isso na minha organização, infelizmente.
BriteSponge
-1

Parece que está dando certo o que faz com que o Oracle comece a avaliar todas as expressões no CASE. Vejo

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;

As duas primeiras consultas são executadas OK.

Serg
fonte