Unerwartete CASE-Bewertungslogik

8

Ich habe immer verstanden, dass die CASEAussage nach einem Kurzschlussprinzip dahingehend funktioniert, dass die Bewertung nachfolgender Schritte nicht erfolgt, wenn ein vorheriger Schritt als wahr bewertet wird. (Diese Antwort wertet die SQL Server-CASE-Anweisung alle Bedingungen aus oder wird sie bei der ersten TRUE-Bedingung beendet? Ist verwandt, scheint diese Situation jedoch nicht abzudecken und bezieht sich auf SQL Server.)

Im folgenden Beispiel möchte ich die MAX(amount)Anzahl der Monate berechnen, die sich je nach Anzahl der Monate zwischen dem Start- und dem Zahlungstermin unterscheiden.

(Dies ist offensichtlich ein konstruiertes Beispiel, aber die Logik hat gültige geschäftliche Argumente im tatsächlichen Code, in dem ich das Problem sehe).

Wenn zwischen dem Start- und dem Zahlungstermin <5 Monate liegen, wird Ausdruck 1 verwendet, andernfalls wird Ausdruck 2 verwendet.

Dies führt zu dem Fehler "ORA-01428: Argument '-1' liegt außerhalb des Bereichs", da 1 Datensatz eine ungültige Datenbedingung aufweist, die zu einem negativen Wert für den Start der BETWEEN-Klausel von ORDER BY führt.

Abfrage 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

Also habe ich mich für diese zweite Abfrage entschieden, um zuerst zu beseitigen, wo immer dies auftreten kann:

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

Leider gibt es ein unerwartetes Verhalten, das bedeutet, dass die Werte, die Ausdruck 1 verwenden würde, validiert werden, obwohl die Anweisung nicht ausgeführt wird, da die negative Bedingung jetzt vom Äußeren abgefangen wird CASE.

Ich kann das Problem umgehen , indem Sie ABSauf dem MONTHS_BETWEENin 1 Expression , aber ich fühle mich wie dies nicht notwendig sein sollte.

Ist dieses Verhalten wie erwartet? Wenn ja, warum, wie es mir unlogisch und eher wie ein Käfer erscheint?


Dadurch werden eine Tabelle und Testdaten erstellt. Die Abfrage ist einfach, dass ich überprüfe, ob der richtige Pfad in der CASEgenommen wird.

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
quelle
3
FWIW SQL Server hat auch seine Macken in diesem Bereich, wo die Dinge nicht ganz so funktionieren, wie angekündigt. Dba.stackexchange.com/a/12945/3690
Martin Smith
3
In SQL Server kann das Platzieren eines Aggregats in einem CASE-Ausdruck dazu führen, dass Teile des Ausdrucks ausgewertet werden, bevor Sie dies erwarten . Ich frage mich, ob hier etwas Ähnliches passiert.
Aaron Bertrand
Das klingt ziemlich nah an dieser Situation. Ich frage mich, was es mit der Logik zur Implementierung von CASE in zwei verschiedenen RDBMS auf sich hat, die zu derselben Art von Effekt führt. Interessant.
BriteSponge
1
Ich frage mich, ob dies erlaubt ist (und ob es das gleiche schlechte Verhalten zeigt):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ᵀᴹ: Die von Ihnen vorgeschlagene Aggregation gibt den Fehler nicht aus. Vielleicht gibt es eine Grenze, wie tief die Bewertung aussehen wird. Spekulation.
BriteSponge

Antworten:

2

Daher war es für mich schwierig festzustellen, was Ihre eigentliche Frage aus dem Beitrag war, aber ich gehe davon aus, dass dies bei der Ausführung der Fall ist:

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

Sie erhalten immer noch ORA-01428: Argument '-1' liegt außerhalb des Bereichs ?

Ich denke nicht, dass dies ein Fehler ist. Ich denke, es ist eine Sache der Betriebsordnung. Oracle muss die Analyse für alle von der Ergebnismenge zurückgegebenen Zeilen durchführen. Dann kann es darum gehen, die Ausgabe zu transformieren.

Ein paar zusätzliche Möglichkeiten, dies zu umgehen, wären, die Zeile mit einer where-Klausel auszuschließen:

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 

Oder Sie können einen Fall wie folgt in Ihre Analyse einbetten:

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

Erläuterung

Ich wünschte, ich könnte eine Dokumentation finden, um die Reihenfolge der Operationen zu sichern, aber ich konnte noch nichts finden.

Die CASEKurzschlussauswertung erfolgt nach Auswertung der Analysefunktion. Die Reihenfolge der Operationen für die betreffende Abfrage wäre:

  1. von der Zahlung
  2. max over ()
  3. Fall.

Da max over()dies also vor dem Fall geschieht, schlägt die Abfrage fehl.

Die Analysefunktionen von Oracle werden als Zeilenquelle betrachtet . Wenn Sie einen Erklärungsplan für Ihre Abfrage ausführen, sollte eine "Fenstersortierung" angezeigt werden, bei der es sich um die analytische, generierende Zeile handelt, die von der vorherigen Zeilenquelle, der Zahlungstabelle, an sie weitergeleitet wird. Eine case-Anweisung ist ein Ausdruck, der für jede Zeile in der Zeilenquelle ausgewertet wird. Es macht also (zumindest für mich) Sinn, dass der Fall nach der Analyse passiert.

Nick S.
quelle
Ich schätze die möglichen Workarounds - es ist immer interessant zu sehen, wie andere Dinge tun. Ich habe jedoch einen einfachen Weg, dies zu umgehen. Die ABS-Funktion funktioniert in meiner Situation. Es ist auch möglich, dass dies nicht wirklich schwierig ist, aber wenn nicht, muss Oracle angeben, dass die breite Konvention bezüglich der Kurzschlusslogik bei analytischen Funktionen nicht gilt.
BriteSponge
Diese Antwort enthält Umgehungsmöglichkeiten und eine logische Erklärung. Ich glaube nicht, dass die Dinge endgültiger werden, und deshalb werde ich dies als Antwort markieren. Danke
BriteSponge
1

SQL definiert, was zu tun ist, nicht wie es zu tun ist. Während Oracle normalerweise die Fallbewertung kurzschließt, ist dies eine Optimierung und wird daher vermieden, wenn der Optimierer der Ansicht ist, dass ein anderer Ausführungspfad eine überlegene Leistung bietet. Ein solcher Optimierungsunterschied wäre zu erwarten, wenn es um Analysen geht.

Der Optimierungsunterschied ist nicht auf den Fall beschränkt. Ihr Fehler kann mit Koaleszenz reproduziert werden, was normalerweise auch einen Kurzschluss bedeuten würde.

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;

Es scheint keine Dokumentation zu geben, die ausdrücklich besagt, dass die Kurzschlussauswertung vom Optimierer ignoriert werden kann. Das Nächste (wenn auch nicht nah genug), das ich finden kann, ist Folgendes :

Alle SQL-Anweisungen verwenden den Optimierer, einen Teil der Oracle-Datenbank, der die effizienteste Methode für den Zugriff auf die angegebenen Daten ermittelt.

Diese Frage zeigt, dass die Kurzschlussbewertung auch ohne Analyse ignoriert wird (obwohl es eine Gruppierung gibt).

Tom Kyte erwähnt, dass Kurzschlüsse in seiner Antwort auf eine Frage zur Bewertung der Reihenfolge der Prädikate ignoriert werden können .

Sie sollten eine SR mit Oracle öffnen. Ich vermute, sie werden es als Dokumentationsfehler akzeptieren und die Dokumentation in der nächsten Version erweitern, um eine Einschränkung bezüglich des Optimierers aufzunehmen.

Leigh Riffel
quelle
Ich wollte eine SR eröffnen, aber es sieht so aus, als ob ich das in meiner Organisation leider nicht tun kann.
BriteSponge
-1

Es sieht so aus, als würde Oracle alle Ausdrücke in CASE auswerten. Sehen

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;

Die ersten beiden Abfragen werden in Ordnung ausgeführt.

Serg
quelle