Logique d'évaluation CASE inattendue

8

J'ai toujours compris que l' CASEénoncé fonctionnait selon un principe de «court-circuit» en ce sens que l'évaluation des étapes suivantes n'a pas lieu si une étape antérieure est évaluée comme vraie. (Cette réponse L'instruction CASE de SQL Server évalue-t-elle toutes les conditions ou quitte-t-elle la première condition VRAIE? Est liée mais ne semble pas couvrir cette situation et concerne SQL Server).

Dans l'exemple suivant, je souhaite calculer le MAX(amount)entre une plage de mois qui diffère en fonction du nombre de mois entre le début et les dates payées.

(Ceci est évidemment un exemple construit mais la logique a un raisonnement commercial valide dans le code réel où je vois le problème).

S'il y a moins de 5 mois entre les dates de début et de paiement, l' expression 1 sera utilisée, sinon l' expression 2 sera utilisée.

Il en résulte l'erreur «ORA-01428: l'argument« -1 »est hors limites» car 1 enregistrement a une condition de données non valide qui se traduit par une valeur négative pour le début de la clause BETWEEN de la commande ORDER BY.

Requête 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

Je suis donc allé pour cette deuxième requête pour éliminer d'abord où que cela puisse se produire:

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

Malheureusement, il existe un comportement inattendu qui signifie que les valeurs que l' expression 1 DEVRAIT utiliser seraient validées, même si l'instruction ne sera pas exécutée car la condition négative est désormais piégée par l'extérieur CASE.

Je peux contourner le problème en utilisant ABSsur MONTHS_BETWEENdans Expression 1 , mais je pense que cela ne devrait pas être nécessaire.

Ce comportement est-il conforme aux attentes? Si oui «pourquoi» car cela me semble illogique et ressemble plus à un bug?


Cela va créer une table et tester les données. La requête consiste simplement à vérifier que le chemin d'accès correct CASEest pris.

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
la source
3
FWIW SQL Server a également ses particularités dans ce domaine où les choses ne fonctionnent pas tout à fait comme annoncé dba.stackexchange.com/a/12945/3690
Martin Smith
3
Dans SQL Server, le fait de placer un agrégat dans une expression CASE peut forcer l'évaluation de parties de l'expression avant de vous y attendre . Je me demande si quelque chose de similaire se passe ici?
Aaron Bertrand
Cela semble assez proche de cette situation. Je me demande ce que c'est que la logique d'implémentation de CASE dans deux SGBDR différents qui mène au même genre d'effet. Intéressant.
BriteSponge
1
Je me demande si cela est autorisé (et si cela montre le même mauvais comportement):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'agrégation que vous proposez ne donne pas l'erreur. Il y a peut-être une limite à la profondeur de l'évaluation. Spéculation.
BriteSponge du

Réponses:

2

Il était donc difficile pour moi de déterminer quelle était votre véritable question dans le post, mais je suppose que lorsque vous exécutez:

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

Vous obtenez toujours ORA-01428: l'argument «-1» est hors de portée ?

Je ne pense pas que ce soit un bug. Je pense que c'est une question d'ordre de fonctionnement. Oracle doit effectuer l'analyse sur toutes les lignes renvoyées par l'ensemble de résultats. Ensuite, il peut aller au fond de la transformation de la sortie.

Deux autres façons de contourner ce problème seraient d'exclure la ligne avec une clause 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 vous pouvez intégrer un cas dans votre analyse comme:

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

Explication

J'aimerais pouvoir trouver de la documentation pour sauvegarder l'ordre de fonctionnement, mais je n'ai rien trouvé ... pour l'instant.

L' CASEévaluation du court-circuit a lieu après l'évaluation de la fonction analytique. L'ordre des opérations pour la requête en question serait:

  1. du paiement
  2. max sur ()
  3. Cas.

Donc, puisque le max over()se produit avant le cas, la requête échoue.

Les fonctions analytiques d'Oracle seraient considérées comme une source de lignes . Si vous exécutez un plan d'explication sur votre requête, vous devriez voir un "tri de fenêtre" qui est l'analyse, générant des lignes, qui sont alimentées par la source de ligne précédente, la table de paiement. Une instruction case est une expression qui est évaluée pour chaque ligne de la source de ligne. Il est donc logique (du moins pour moi) que le cas se produise après l'analyse.

Nick S
la source
J'apprécie les contournements potentiels - c'est toujours intéressant de voir comment les autres font les choses. Cependant, j'ai et un moyen facile de contourner cela; la fonction ABS fonctionne dans ma situation. En outre, il est possible que ce ne soit pas vraiment un problème, mais sinon, Oracle doit indiquer que la large convention concernant la logique de «court-circuit» ne s'applique pas dans le cas des fonctions analytiques.
BriteSponge
Cette réponse a des contournements et une explication logique. Je ne pense pas que les choses deviendront plus définitives et je vais donc marquer cela comme la réponse. Merci
BriteSponge
1

SQL définit ce qu'il faut faire, pas comment le faire. Bien qu'Oracle court-circuite normalement l'évaluation des cas, il s'agit d'une optimisation et sera donc évitée si l'optimiseur estime qu'un chemin d'exécution différent fournit des performances supérieures. Une telle différence d'optimisation serait attendue lorsque des analyses sont impliquées.

La différence d'optimisation n'est pas limitée au cas. Votre erreur peut être reproduite en utilisant la fusion, ce qui normalement court-circuiterait également.

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;

Il ne semble pas y avoir de documentation expliquant explicitement que l’évaluation à court terme peut être ignorée par l’optimiseur. La chose la plus proche (mais pas assez proche) que je peux trouver est la suivante :

Toutes les instructions SQL utilisent l'optimiseur, une partie d'Oracle Database qui détermine le moyen le plus efficace d'accéder aux données spécifiées.

Cette question montre que l'évaluation des courts-circuits est ignorée même sans analyse (bien qu'il y ait un regroupement).

Tom Kyte mentionne que le court-circuit peut être ignoré dans sa réponse à une question sur l' ordre d'évaluation des prédicats .

Vous devez ouvrir un SR avec Oracle. Je soupçonne qu'ils l'accepteront comme bogue de documentation et amélioreront la documentation dans la prochaine version pour inclure une mise en garde concernant l'optimiseur.

Leigh Riffel
la source
J'allais ouvrir un SR, mais il semble que je ne pourrai pas le faire dans mon organisation, malheureusement.
BriteSponge
-1

Il semble que ce soit un fenêtrage qui pousse Oracle à commencer à évaluer toutes les expressions dans CASE. Voir

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;

Les deux premières requêtes s'exécutent correctement.

Serg
la source