Logika evaluasi KASUS yang tidak terduga

8

Saya selalu mengerti bahwa CASEpernyataan tersebut bekerja pada prinsip 'hubungan pendek' dalam evaluasi bahwa langkah-langkah selanjutnya tidak terjadi jika langkah sebelumnya dievaluasi menjadi benar. (Jawaban ini Apakah pernyataan KASUS SQL Server mengevaluasi semua kondisi atau keluar pada kondisi BENAR pertama? Terkait tetapi tampaknya tidak mencakup situasi ini dan terkait dengan SQL Server).

Dalam contoh berikut, saya ingin menghitung MAX(amount)rentang bulan yang berbeda berdasarkan berapa bulan antara tanggal mulai dan tanggal berbayar.

(Ini jelas merupakan contoh yang dibangun tetapi logika memiliki alasan bisnis yang valid dalam kode aktual di mana saya melihat masalah).

Jika ada <5 bulan antara tanggal mulai dan tanggal pembayaran maka Ekspresi 1 akan digunakan jika tidak, Ekspresi 2 akan digunakan.

Ini menghasilkan kesalahan "ORA-01428: argumen '-1' berada di luar kisaran" karena 1 catatan memiliki kondisi data yang tidak valid yang menghasilkan nilai negatif untuk dimulainya klausa BETWEEN dari ORDER BY.

Pertanyaan 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

Jadi saya pergi untuk kueri kedua ini untuk menghilangkan dulu di mana saja hal ini dapat terjadi:

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

Sayangnya, ada beberapa perilaku tak terduga yang berarti bahwa nilai-nilai Ekspresi 1 AKAN digunakan divalidasi, meskipun pernyataan itu tidak akan dieksekusi karena kondisi negatif sekarang terjebak oleh luar CASE.

Saya bisa mendapatkan sekitar masalah dengan menggunakan ABSpada MONTHS_BETWEENdi Expression 1 , tapi aku merasa seperti ini harus tidak perlu.

Apakah perilaku ini seperti yang diharapkan? Jika demikian 'mengapa' karena tampaknya tidak masuk akal bagi saya dan lebih seperti bug?


Ini akan membuat tabel dan menguji data. Pertanyaannya hanyalah saya memeriksa bahwa jalur yang benar CASEsedang diambil.

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
sumber
3
FWIW SQL Server juga memiliki keanehan di bidang ini di mana segala sesuatu tidak berjalan seperti yang diiklankan dba.stackexchange.com/a/12945/3690
Martin Smith
3
Di SQL Server, menempatkan agregat di dalam ekspresi KASUS dapat memaksa bagian ekspresi dievaluasi sebelum yang Anda harapkan . Saya ingin tahu apakah hal serupa terjadi di sini?
Aaron Bertrand
Kedengarannya cukup dekat dengan situasi ini. Membuat saya bertanya-tanya apa itu tentang logika untuk menerapkan KASUS dalam dua RDBMS berbeda yang mengarah ke jenis efek yang sama. Menarik.
BriteSponge
1
Saya ingin tahu apakah ini diizinkan (dan apakah itu menunjukkan perilaku sakit yang sama):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ᵀᴹ: Agregasi yang Anda sarankan tidak memberikan kesalahan. Mungkin ada batasan untuk seberapa 'dalam' evaluasi akan terlihat. Spekulasi.
BriteSponge

Jawaban:

2

Jadi sulit bagi saya untuk menentukan apa pertanyaan Anda yang sebenarnya dari pos, tetapi saya berasumsi bahwa ketika Anda mengeksekusi:

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

Anda masih mendapatkan ORA-01428: argumen '-1' di luar jangkauan ?

Saya rasa ini bukan bug. Saya pikir ini adalah urutan operasi. Oracle perlu melakukan analisis pada semua baris yang dikembalikan oleh resultset. Maka itu bisa turun ke seluk-beluk dari mengubah output.

Beberapa cara tambahan untuk ini adalah dengan mengecualikan baris dengan klausa di mana:

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 

Atau Anda dapat menyematkan sebuah kasus dalam analitik Anda seperti:

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

Penjelasan

Saya berharap saya dapat menemukan beberapa dokumentasi untuk mendukung urutan operasi, tetapi saya belum dapat menemukan apa pun ... belum.

The CASEarus pendek evaluasi yang terjadi setelah fungsi analitik dievaluasi. Urutan operasi untuk kueri yang dimaksud adalah:

  1. dari pembayaran
  2. maks. melebihi ()
  3. kasus.

Jadi sejak max over()terjadi sebelum kasus, permintaan gagal.

Fungsi analitik Oracle akan dianggap sebagai sumber baris . Jika Anda menjalankan rencana penjelasan pada kueri Anda, Anda akan melihat "jenis jendela" yang merupakan analitik, menghasilkan baris, yang diumpankan kepadanya oleh sumber baris sebelumnya, tabel pembayaran. Pernyataan kasus adalah ekspresi yang dievaluasi untuk setiap baris di sumber baris. Jadi masuk akal (setidaknya bagi saya), bahwa kasus terjadi setelah analitik.

Nick S
sumber
Saya menghargai potensi kerja di sekitarnya - selalu menarik untuk melihat bagaimana orang lain melakukan sesuatu. Namun, saya punya dan cara mudah untuk mengatasi ini; fungsi ABS berfungsi dalam situasi saya. Juga, mungkin ini tidak benar-benar buruk tetapi jika tidak maka Oracle perlu menyatakan bahwa konvensi luas tentang logika 'korsleting' tidak berlaku dalam kasus fungsi analitik.
BriteSponge
Jawaban ini memiliki cara kerja dan penjelasan yang logis. Saya tidak berpikir hal-hal akan menjadi lebih pasti dan jadi saya akan menandai ini sebagai jawabannya. Terima kasih
BriteSponge
1

SQL mendefinisikan apa yang harus dilakukan, bukan bagaimana melakukannya. Sementara biasanya Oracle akan melakukan evaluasi kasus hubungan pendek, ini adalah optimasi dan karenanya akan dihindari jika optimizer percaya jalur eksekusi yang berbeda memberikan kinerja yang unggul. Perbedaan optimasi seperti itu akan diharapkan ketika analitik terlibat.

Perbedaan optimasi tidak terbatas pada kasus. Kesalahan Anda dapat direproduksi menggunakan coalesce, yang biasanya juga mengalami hubungan pendek.

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;

Tampaknya tidak ada dokumentasi yang secara eksplisit mengatakan bahwa evaluasi arus pendek dapat diabaikan oleh pengoptimal. Hal terdekat (walaupun tidak cukup dekat) yang dapat saya temukan adalah ini :

Semua pernyataan SQL menggunakan optimizer, bagian dari Oracle Database yang menentukan cara paling efisien untuk mengakses data yang ditentukan.

Pertanyaan ini menunjukkan evaluasi hubungan pendek diabaikan bahkan tanpa analitik (meskipun ada pengelompokan).

Tom Kyte menyebutkan bahwa hubungan arus pendek dapat diabaikan dalam tanggapannya terhadap pertanyaan tentang urutan penilaian predikat .

Anda harus membuka SR dengan Oracle. Saya menduga mereka akan menerimanya sebagai bug dokumentasi, dan meningkatkan dokumentasi di versi berikutnya untuk memasukkan peringatan tentang pengoptimal.

Leigh Riffel
sumber
Saya akan membuka SR tetapi sepertinya saya tidak akan bisa melakukan itu di organisasi saya sayangnya.
BriteSponge
-1

Sepertinya itu windowing yang membuat Oracle mulai mengevaluasi semua ekspresi dalam CASE. Lihat

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;

Dua kueri pertama dijalankan OK.

Serg
sumber