予期しないCASE評価ロジック


8

CASE前のステップが真であると評価された場合、後続のステップの評価は行われないという点で、ステートメントは「短絡」原理に基づいて機能することを常に理解していました。(この回答は、SQL Server CASEステートメントがすべての条件を評価するか、最初のTRUE条件で終了しますか?は関連していますが、この状況をカバーしておらず、SQL Serverに関連しています)。

次の例でMAX(amount)は、開始日と支払い日の間の月数に基づいて異なる月の範囲の間を計算したいと思います。

(これは明らかに構築された例ですが、ロジックは、私が問題を見る実際のコードで有効なビジネス推論を持っています)。

開始日と支払い日の間の期間が5か月未満の場合は、式1が使用されます。それ以外の場合は、式2が使用されます。

これにより、「ORA-01428:引数 '-1'は範囲外です」というエラーが発生します。これは、1つのレコードに無効なデータ条件があり、ORDER BYのBETWEEN句の開始が負の値になるためです。

クエリ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

そこで、この2番目のクエリを実行して、これが発生する可能性のある場所を最初に排除しました。

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

残念ながら、予期しない動作が発生し、式1が使用する値が検証されますが、負の条件が外側でトラップされるため、ステートメントは実行されませんCASE

式1のを使用ABSすることMONTHS_BETWEENで問題を回避できますが、これは不要なはずです。

この動作は期待どおりですか?もしそうなら「なぜ」それは私にとって非論理的で、バグのように見えますか?


これにより、テーブルとテストデータが作成されます。クエリは、の正しいパスCASEが使用されていることを確認するだけです。

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には、この領域にも奇妙な点があり、宣伝されているとおりにうまく
Martin Smith

3
SQL Serverでは、CASE式の内部に集計を配置すると、式の一部が予想よりも早く評価される可能性があります。ここで似たようなことが起こっているのでしょうか。
アーロンバートランド

それはこの状況にかなり近いように聞こえます。同じ種類の効果につながる2つの異なるRDBMSでCASEを実装するためのロジックについて、私は疑問に思います。面白い。
BriteSponge

1
これが許可されているかどうか(そしてそれが同じ悪い行動を示すかどうか):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ᵀᴹ16年

@ypercubeᵀᴹ:提案する集計ではエラーは発生しません。たぶん、評価がどの程度「深く」見えるかには限界があるかもしれません。投機。
BriteSponge

回答:


2

だから私はあなたの実際の質問が投稿から何であったかを判断することは困難でしたが、私はあなたが実行するときそれがそうだと思います:

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

それでもORA-01428が表示されます:引数 '-1'は範囲外ですか?

これはバグだとは思いません。動作順のものだと思います。Oracleは、結果セットによって返されたすべての行に対して分析を行う必要があります。次に、出力を変換する重要な要点に到達できます。

これを回避するいくつかの追加の方法は、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 

または、次のようにケースをアナリティクスに埋め込むことができます。

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

説明

操作の順序をバックアップするためのドキュメントが見つかればいいのですが、まだ何も見つかりませんでした。

CASE分析関数が評価された後に短絡評価が起こります。問題のクエリの操作の順序は次のようになります。

  1. お支払いから
  2. max over()
  3. 場合。

したがって、はmax over()ケースの前に発生するため、クエリは失敗します。

Oracleの分析関数は、行ソースと見なされます。クエリに対してExplain Planを実行すると、分析である「ウィンドウソート」が表示され、前の行ソースである支払いテーブルによって供給される行が生成されます。caseステートメントは、行ソースの各行に対して評価される式です。したがって、(少なくとも私にとっては)ケースが分析後に発生することは理にかなっています。


潜在的な回避策に感謝します-他の人がどうやってやっているかを見るのはいつも面白いです。ただし、これを回避する簡単な方法があります。ABS機能は私の状況で機能します。また、これが実際にバグではない可能性もありますが、そうでない場合、Oracleは「短絡」ロジックに関する幅広い規約が分析関数の場合には適用されないことを示す必要があります。
BriteSponge

この回答には回避策と論理的な説明があります。決定的なものになるとは思わないので、これを答えとしてマークします。ありがとう
BriteSponge

1

SQLは、実行する方法ではなく、実行することを定義します。通常、Oracleはケース評価を短絡しますが、これは最適化であるため、別の実行パスが優れたパフォーマンスを提供するとオプティマイザが信じる場合は回避されます。このような最適化の違いは、分析が関係する場合に予想されます。

最適化の違いは大文字と小文字に限定されません。あなたのエラーは合体を使って再現できますが、これは通常は短絡にもなります。

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;

オプティマイザは短期間の評価を無視できると明示的に述べているドキュメントはないようです。私が見つけることができる最も近いもの(十分に近いわけではありません)はこれです:

すべてのSQL文は、指定されたデータにアクセスする最も効率的な手段を決定するOracleデータベースの一部であるオプティマイザを使用します。

この質問は、分析がなくても短絡評価が無視されることを示しています(グループ化はあります)。

トム・カイト氏は、述語評価の順序に関する質問に対する彼の回答では、短絡は無視できると述べています。

OracleでSRを開く必要があります。私は彼らがそれをドキュメンテーションのバグとして受け入れ、オプティマイザについての警告を含むように次のバージョンでドキュメンテーションを拡張すると思います。


私はSRを開くつもりでしたが、残念ながら私の組織ではそれを行うことができないようです。
BriteSponge

-1

OracleがCASEのすべての式の評価を開始する原因となっているのはウィンドウ処理のようです。見る

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;

最初の2つのクエリは正常に実行されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.