Logica di valutazione CASE imprevista


8

Ho sempre capito che l' CASEaffermazione ha funzionato secondo un principio di "cortocircuito" in quanto la valutazione delle fasi successive non ha luogo se una fase precedente è valutata come vera. (Questa risposta L'istruzione CASE di SQL Server valuta tutte le condizioni o esce alla prima condizione VERA? È correlata ma non sembra coprire questa situazione e si riferisce a SQL Server).

Nel seguente esempio, desidero calcolare l' MAX(amount)intervallo tra un intervallo di mesi che differisce in base al numero di mesi tra l'inizio e le date pagate.

(Questo è ovviamente un esempio costruito ma la logica ha un valido ragionamento commerciale nel codice attuale in cui vedo il problema).

Se ci sono <5 mesi tra l'inizio e le date di pagamento, verrà utilizzata l' Espressione 1 , altrimenti verrà utilizzata l' Espressione 2 .

Ciò provoca l'errore "ORA-01428: l'argomento '-1' non è compreso nell'intervallo" perché 1 record presenta una condizione dati non valida che determina un valore negativo per l'inizio della clausola BETWEEN di ORDER BY.

Query 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

Quindi sono andato per questa seconda query per eliminare prima ovunque ciò possa accadere:

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

Sfortunatamente, c'è un comportamento inaspettato che significa che i valori di Expression 1 DEVONO essere validati, anche se l'istruzione non verrà eseguita perché la condizione negativa è ora intrappolata dall'esterno CASE.

Posso aggirare il problema utilizzando ABSil MONTHS_BETWEENdi Espressione 1 , ma mi sento come questo dovrebbe essere inutile.

Questo comportamento è come previsto? In tal caso "perché" come mi sembra illogico e più simile a un bug?


Ciò creerà una tabella e testerà i dati. La query è semplicemente che sto controllando che CASEvenga preso il percorso corretto in .

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

3
FWIW SQL Server ha anche le sue stranezze in quest'area in cui le cose non funzionano come pubblicizzato dba.stackexchange.com/a/12945/3690
Martin Smith,

3
In SQL Server, l'inserimento di un aggregato all'interno di un'espressione CASE può forzare la valutazione di parti dell'espressione prima del previsto . Mi chiedo se qualcosa di simile sta succedendo qui?
Aaron Bertrand

Sembra abbastanza vicino a questa situazione. Mi chiedo che cos'è la logica per l'implementazione di CASE in due diversi RDBMS che porta allo stesso tipo di effetto. Interessante.
BriteSponge,

1
Mi chiedo se questo è permesso (e se mostra lo stesso comportamento malato):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ᵀᴹ: l'aggregazione che suggerisci non dà l'errore. Forse c'è un limite a quanto "profonda" sarà la valutazione. Speculazione.
BriteSponge,

Risposte:


2

Quindi è stato difficile per me determinare quale fosse la tua vera domanda dal post, ma suppongo che quando esegui:

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

Ottieni ancora ORA-01428: l'argomento '-1' è fuori portata ?

Non penso che questo sia un bug. Penso che sia un ordine di operazione. Oracle deve eseguire l'analisi su tutte le righe restituite dal gruppo di risultati. Quindi può arrivare fino all'ottimo di trasformare l'output.

Un paio di modi per aggirare questo sarebbe quello di escludere la riga con una clausola 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 

Oppure potresti incorporare un caso nell'analitica come:

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

Spiegazione

Vorrei poter trovare della documentazione per eseguire il backup dell'ordine delle operazioni, ma non sono ancora riuscito a trovare nulla ...

La CASEvalutazione del cortocircuito avviene dopo la valutazione della funzione analitica. L'ordine delle operazioni per la query in questione sarebbe:

  1. dal pagamento
  2. max over ()
  3. Astuccio.

Quindi, poiché max over()accade prima del caso, la query ha esito negativo.

Le funzioni analitiche di Oracle sarebbero considerate una sorgente di riga . Se esegui un piano di spiegazione sulla tua query, dovresti vedere un "ordinamento di finestre" che è l'analitico, che genera righe, che vengono alimentate dalla sorgente di riga precedente, la tabella di pagamento. Un'istruzione case è un'espressione che viene valutata per ogni riga nell'origine riga. Quindi ha senso (almeno per me) che il caso si verifichi dopo l'analitica.


Apprezzo il potenziale aggirare il problema - è sempre interessante vedere come gli altri fanno le cose. Tuttavia, ho e un modo semplice per aggirare questo; la funzione ABS funziona nella mia situazione. Inoltre, è possibile che ciò non sia effettivamente adeguato, ma in caso contrario Oracle deve dichiarare che l'ampia convenzione relativa alla logica del "cortocircuito" non si applica nel caso di funzioni analitiche.
BriteSponge,

Questa risposta ha delle soluzioni e una spiegazione logica. Non penso che le cose diventeranno più definitive e quindi lo segnerò come risposta. Grazie
BriteSponge

1

SQL definisce cosa fare, non come farlo. Mentre normalmente Oracle eseguirà il cortocircuito della valutazione dei casi, questa è un'ottimizzazione e pertanto sarà evitata se l'ottimizzatore ritiene che un diverso percorso di esecuzione fornisca prestazioni superiori. Una simile differenza di ottimizzazione sarebbe prevedibile quando si tratta di analisi.

La differenza di ottimizzazione non si limita al caso. Il tuo errore può essere riprodotto usando la coalescenza, che normalmente sarebbe anche in corto 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;

Non sembra esserci alcuna documentazione che affermi esplicitamente che la valutazione del cortocircuito può essere ignorata dall'ottimizzatore. La cosa più vicina (anche se non abbastanza vicino) che posso trovare è questa :

Tutte le istruzioni SQL utilizzano l'ottimizzatore, una parte del database Oracle che determina i mezzi più efficienti per accedere ai dati specificati.

Questa domanda mostra che la valutazione del corto circuito viene ignorata anche senza analisi (sebbene ci sia raggruppamento).

Tom Kyte afferma che il corto circuito può essere ignorato nella sua risposta a una domanda sull'ordine di valutazione del predicato .

Dovresti aprire un SR con Oracle. Sospetto che lo accetteranno come un bug di documentazione e miglioreranno la documentazione nella prossima versione per includere un avvertimento sull'ottimizzatore.


Stavo per aprire un SR ma sembra che non sarò in grado di farlo purtroppo nella mia organizzazione.
BriteSponge,

-1

Sembra che sia una finestra a indurre Oracle a iniziare a valutare tutte le espressioni in CASE. Vedere

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;

Le prime due query funzionano correttamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.