AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 136214
Accepted
BriteSponge
BriteSponge
Asked: 2016-04-23 02:38:18 +0800 CST2016-04-23 02:38:18 +0800 CST 2016-04-23 02:38:18 +0800 CST

Lógica de avaliação CASE inesperada

  • 772

Sempre entendi que a CASEdeclaração funcionava com base no 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 TRUE? está relacionada, mas não parece cobrir esta situação e está relacionada ao SQL Server).

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

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

Se houver < 5 meses entre as datas de início e 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 tem uma condição de dados inválido que resulta em um valor negativo para o início da cláusula BETWEEN do 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, optei por esta segunda consulta para eliminar primeiro 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 que a Expressão 1 usaria são validados, mesmo que a instrução não seja executada porque a condição negativa agora é capturada pelo CASE.

Posso contornar o problema usando ABSon the MONTHS_BETWEENem Expression 1 , mas acho que isso deve ser desnecessário.

Esse comportamento é o esperado? Em caso afirmativo, 'por que', pois 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 no CASEestá sendo seguido.

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
oracle
  • 3 3 respostas
  • 999 Views

3 respostas

  • Voted
  1. Best Answer
    Nick S
    2016-04-27T12:11:59+08:002016-04-27T12:11:59+08:00

    Portanto, foi difícil para mim determinar qual era sua pergunta real na postagem, mas presumo que 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 obtém ORA-01428: o argumento '-1' está fora do intervalo ?

    Eu não acho que isso é um bug. Eu acho que é uma coisa de ordem de operação. A Oracle precisa fazer análises em todas as linhas retornadas pelo conjunto de resultados. Em seguida, pode 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

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

    A CASEavaliação do 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. máximo acima()
    3. caso.

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

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

    • 2
  2. Leigh Riffel
    2016-04-27T08:06:05+08:002016-04-27T08:06:05+08:00

    SQL define o que fazer, não como fazer. Embora normalmente o Oracle interrompa a avaliação do caso, isso é uma otimização e, portanto, será evitada se o otimizador acreditar que um caminho de execução diferente oferece 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, que normalmente também causaria 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;
    

    Não parece haver nenhuma documentação dizendo explicitamente que a avaliação de curto-circuito pode ser ignorada pelo otimizador. A coisa mais próxima (embora não perto o suficiente) que posso encontrar é esta :

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

    Esta questão mostra a avaliação de curto-circuito sendo ignorada mesmo sem análise (embora haja agrupamento).

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

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

    • 1
  3. Serg
    2016-05-02T00:32:59+08:002016-05-02T00:32:59+08:00

    Parece que é o janelamento que faz o Oracle começar a avaliar todas as expressões em 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;
    

    As duas primeiras consultas são executadas corretamente.

    • -1

relate perguntas

  • Backups de banco de dados no Oracle - Exportar o banco de dados ou usar outras ferramentas?

  • ORDER BY usando prioridades personalizadas para colunas de texto

  • Interface sqlplus confortável? [fechado]

  • Como encontrar as instruções SQL mais recentes no banco de dados?

  • Como posso consultar nomes usando expressões regulares?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve