Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Rahasia Kotor dari Ekspresi KASUS

CASE ekspresi adalah salah satu konstruksi favorit saya di T-SQL. Ini cukup fleksibel, dan terkadang merupakan satu-satunya cara untuk mengontrol urutan di mana SQL Server akan mengevaluasi predikat.
Namun, sering disalahpahami.

Apa itu T-SQL CASE Expression?

Dalam T-SQL, CASE adalah ekspresi yang mengevaluasi satu atau lebih ekspresi yang mungkin dan mengembalikan ekspresi pertama yang sesuai. Istilah ekspresi mungkin sedikit berlebihan di sini, tetapi pada dasarnya itu adalah segala sesuatu yang dapat dievaluasi sebagai nilai skalar tunggal, seperti variabel, kolom, literal string, atau bahkan output dari fungsi bawaan atau skalar. .

Ada dua bentuk CASE di T-SQL:

  • Ekspresi CASE sederhana – ketika Anda hanya perlu mengevaluasi kesetaraan:

    CASE <input> WHEN <eval> THEN <return> … [ELSE <return>] END

  • Menelusuri ekspresi CASE – ketika Anda perlu mengevaluasi ekspresi yang lebih kompleks, seperti ketidaksetaraan, LIKE, atau IS NOT NULL:

    CASE WHEN <input_bool> THEN <return> … [ELSE <return>] END

Ekspresi yang dikembalikan selalu berupa nilai tunggal, dan tipe data keluaran ditentukan oleh prioritas tipe data.

Seperti yang saya katakan, ekspresi CASE sering disalahpahami; berikut beberapa contohnya:

CASE adalah ekspresi, bukan pernyataan

Mungkin tidak penting bagi kebanyakan orang, dan mungkin ini hanya sisi bertele-tele saya, tetapi banyak orang menyebutnya CASE pernyataan – termasuk Microsoft, yang dokumentasinya menggunakan pernyataan dan ekspresi bergantian sewaktu-waktu. Menurut saya ini agak mengganggu (seperti baris/rekam dan kolom/bidang ) dan, meskipun sebagian besar semantik, tetapi ada perbedaan penting antara ekspresi dan pernyataan:ekspresi mengembalikan hasil. Ketika orang memikirkan CASE sebagai pernyataan , itu mengarah ke eksperimen dalam pemendekan kode seperti ini:

SELECT CASE [status] WHEN 'A' THEN StatusLabel ='Authorized', LastEvent =AuthorizedTime WHEN 'C' THEN StatusLabel ='Completed', LastEvent =CompletedTime ENDFROM dbo.some_table;

Atau ini:

PILIH KASUS KETIKA @foo =1 MAKA (PILIH foo, bilah FROM dbo.fizzbuzz)ELSE (PILIH blat, mort FROM dbo.splunge)END;

Jenis logika kontrol aliran ini dimungkinkan dengan CASE pernyataan dalam bahasa lain (seperti VBScript), tetapi tidak dalam CASE Trans Transact-SQL ekspresi . Untuk menggunakan CASE dalam logika kueri yang sama, Anda harus menggunakan CASE ekspresi untuk setiap kolom keluaran:

SELECT StatusLabel =CASE [status] WHEN 'A' THEN 'Authorized' WHEN 'C' THEN 'Completed' END, LastEvent =CASE [status] WHEN 'A' THEN AuthorizedTime WHEN 'C' THEN CompletedTime ENDFROM dbo.some_table;

CASE tidak selalu korsleting

Dokumentasi resmi pernah menyiratkan bahwa seluruh ekspresi akan mengalami hubungan arus pendek, yang berarti ia akan mengevaluasi ekspresi dari kiri ke kanan, dan berhenti mengevaluasi saat mencapai kecocokan:

Pernyataan CASE [sic!] mengevaluasi kondisinya secara berurutan dan berhenti dengan kondisi pertama yang kondisinya terpenuhi.

Namun, ini tidak selalu benar. Dan untuk pujiannya, dalam versi yang lebih baru, halaman tersebut mencoba menjelaskan satu skenario di mana ini tidak dijamin. Tapi itu hanya sebagian dari cerita:

Dalam beberapa situasi, ekspresi dievaluasi sebelum pernyataan CASE [sic!] menerima hasil ekspresi sebagai inputnya. Kesalahan dalam mengevaluasi ekspresi ini mungkin terjadi. Ekspresi agregat yang muncul dalam argumen WHEN ke pernyataan CASE [sic!] dievaluasi terlebih dahulu, kemudian diberikan ke pernyataan CASE [sic!]. Misalnya, kueri berikut menghasilkan kesalahan bagi dengan nol saat menghasilkan nilai agregat MAX. Ini terjadi sebelum mengevaluasi ekspresi CASE.

Contoh pembagian dengan nol cukup mudah untuk direproduksi, dan saya menunjukkannya dalam jawaban ini di dba.stackexchange.com:

DECLARE @i INT =1;SELECT CASE WHEN @i =1 THEN 1 ELSE MIN(1/0) END;

Hasil:

Msg 8134, Level 16, State 1
Ditemukan kesalahan pembagian dengan nol.

Ada solusi sepele (seperti ELSE (SELECT MIN(1/0)) END ), tetapi ini benar-benar mengejutkan banyak orang yang belum menghafal kalimat-kalimat di atas dari Books Online. Saya pertama kali mengetahui skenario khusus ini dalam percakapan di daftar distribusi email pribadi oleh Itzik Ben-Gan (@ItzikBenGan), yang pada awalnya diberitahukan oleh Jaime Lafargue. Saya melaporkan bug di Connect #690017 :CASE / COALESCE tidak akan selalu mengevaluasi dalam urutan tekstual; itu dengan cepat ditutup sebagai "Dengan Desain." Paul White (blog | @SQL_Kiwi) kemudian mengajukan Connect #691535 :Agregat Jangan Ikuti Semantik KASUS, dan ditutup sebagai "Tetap." Perbaikannya, dalam hal ini, adalah klarifikasi di artikel Books Online; yaitu, cuplikan yang saya salin di atas.

Perilaku ini dapat menghasilkan dirinya sendiri dalam beberapa skenario lain yang kurang jelas juga. Misalnya, Connect #780132 :FREETEXT() tidak menghormati urutan evaluasi dalam pernyataan CASE (tidak ada agregat yang terlibat) menunjukkan bahwa, CASE urutan evaluasi juga tidak dijamin menjadi kiri-ke-kanan saat menggunakan fungsi teks lengkap tertentu. Pada item itu, Paul White berkomentar bahwa dia juga mengamati hal serupa menggunakan LAG() . yang baru fungsi diperkenalkan di SQL Server 2012. Saya tidak memiliki repro yang berguna, tapi saya percaya padanya, dan saya tidak berpikir kita telah menemukan semua kasus tepi di mana ini mungkin terjadi.

Jadi, ketika agregat atau layanan non-asli seperti Pencarian Teks Lengkap terlibat, jangan membuat asumsi apa pun tentang hubungan arus pendek dalam CASE ekspresi.

RAND() dapat dievaluasi lebih dari sekali

Saya sering melihat orang menulis sederhana CASE ekspresi, seperti ini:

PILIH KASUS @variabel WHEN 1 THEN 'foo' WHEN 2 THEN 'bar'END

Penting untuk dipahami bahwa ini akan dijalankan sebagai dicari CASE ekspresi, seperti ini:

SELECT CASE WHEN @variable =1 THEN 'foo' WHEN @variable =2 THEN 'bar'END

Alasan penting untuk memahami bahwa ekspresi yang dievaluasi akan dievaluasi beberapa kali, karena ekspresi sebenarnya dapat dievaluasi beberapa kali. Ketika ini adalah variabel, atau konstanta, atau referensi kolom, ini tidak mungkin menjadi masalah nyata; namun, hal-hal dapat berubah dengan cepat jika itu adalah fungsi non-deterministik. Pertimbangkan bahwa ekspresi ini menghasilkan SMALLINT antara 1 dan 3; lanjutkan dan jalankan berkali-kali, dan Anda akan selalu mendapatkan salah satu dari tiga nilai tersebut:

PILIH KONVERSI(SMALLINT, 1+RAND()*3);

Sekarang, masukkan ini ke dalam CASE sederhana ekspresi, dan jalankan belasan kali – akhirnya Anda akan mendapatkan hasil NULL :

SELECT [result] =CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three'END;

Bagaimana ini terjadi? Nah, seluruh CASE ekspresi diperluas ke ekspresi yang dicari, sebagai berikut:

SELECT [hasil] =CASE WHEN CONVERT(SMALLINT, 1+RAND()*3) =1 THEN 'one' WHEN CONVERT(SMALLINT, 1+RAND()*3) =2 THEN 'two' WHEN CONVERT( SMALLINT, 1+RAND()*3) =3 THEN 'three' ELSE NULL -- ini selalu tersirat di sanaEND;

Pada gilirannya, yang terjadi adalah setiap WHEN klausa mengevaluasi dan memanggil RAND() independen – dan dalam setiap kasus dapat menghasilkan nilai yang berbeda. Katakanlah kita memasukkan ekspresi, dan kita memeriksa WHEN pertama klausa, dan hasilnya adalah 3; kami melewatkan klausa itu dan melanjutkan. Dapat dibayangkan bahwa dua klausa berikutnya akan menghasilkan 1 ketika RAND() dievaluasi lagi – dalam hal ini tidak ada kondisi yang dievaluasi benar, jadi ELSE mengambil alih.

Ekspresi lain dapat dievaluasi lebih dari sekali

Masalah ini tidak terbatas pada RAND() fungsi. Bayangkan gaya non-determinisme yang sama datang dari target bergerak ini:

SELECT [crypt_gen] =1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] =LEFT(NEWID(),2), [checksum] =ABS(CHECKSUM(NEWID())%3); 

Ekspresi ini jelas dapat menghasilkan nilai yang berbeda jika dievaluasi beberapa kali. Dan dengan pencarian CASE ekspresi, akan ada saat ketika setiap evaluasi ulang terjadi keluar dari pencarian khusus untuk WHEN saat ini , dan akhirnya tekan ELSE ayat. Untuk melindungi diri Anda dari hal ini, satu opsi adalah selalu membuat kode keras ELSE eksplisit Anda sendiri; hanya berhati-hatilah dengan nilai fallback yang Anda pilih untuk dikembalikan, karena ini akan memiliki beberapa efek miring jika Anda mencari distribusi yang merata. Pilihan lain adalah dengan hanya mengubah WHEN terakhir klausa ke ELSE , tetapi ini masih akan menyebabkan distribusi yang tidak merata. Opsi yang disukai, menurut pendapat saya, adalah mencoba dan memaksa SQL Server untuk mengevaluasi kondisi satu kali (meskipun ini tidak selalu memungkinkan dalam satu permintaan). Misalnya, bandingkan dua hasil ini:

-- Query A:ekspresi direferensikan langsung dalam CASE; no ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM sys.all_columns ) SEBAGAI y KELOMPOK OLEH x; -- Query B:tambahan klausa ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2 ' ELSE '2' END FROM sys.all_columns) AS y GROUP BY x; -- Query C:Final WHEN dikonversi ke ELSE:SELECT x, COUNT(*) FROM( SELECT x =CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' ELSE '2 ' END FROM sys.all_columns) AS y GROUP BY x; -- Query D:Dorong evaluasi NEWID() ke subquery:SELECT x, COUNT(*) FROM( SELECT x =CASE x WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM ( SELECT x =ABS(CHECKSUM(NEWID())%3) FROM sys.all_columns ) AS x) AS y GROUP BY x;

Distribusi:

Nilai Kueri A Kueri B Kueri C Kueri D NULL 2,572 – – – 0 2.923 2,900 2.928 2.949 1 1,946 1,959 1,927 2.896 2 1.295 3.877 3.881 2,891

Distribusi nilai dengan teknik kueri berbeda

Dalam hal ini, saya mengandalkan fakta bahwa SQL Server memilih untuk mengevaluasi ekspresi dalam subquery dan tidak memasukkannya ke CASE yang dicari ekspresi, tetapi ini hanya untuk menunjukkan bahwa distribusi dapat dipaksakan menjadi lebih merata. Pada kenyataannya, ini mungkin tidak selalu menjadi pilihan pengoptimal, jadi jangan belajar dari trik kecil ini. :-)

CHOOSE() juga terpengaruh

Anda akan melihat bahwa jika Anda mengganti CHECKSUM(NEWID()) ekspresi dengan RAND() ekspresi, Anda akan mendapatkan hasil yang sama sekali berbeda; terutama, yang terakhir hanya akan mengembalikan satu nilai. Ini karena RAND() , seperti GETDATE() dan beberapa fungsi bawaan lainnya, diberikan perlakuan khusus sebagai konstanta runtime, dan hanya dievaluasi sekali per referensi untuk seluruh baris. Perhatikan bahwa itu masih dapat mengembalikan NULL seperti kueri pertama dalam contoh kode sebelumnya.

Masalah ini juga tidak terbatas pada CASE ekspresi; Anda dapat melihat perilaku serupa dengan fungsi bawaan lainnya yang menggunakan semantik dasar yang sama. Misalnya, CHOOSE hanyalah gula sintaksis untuk pencarian CASE yang lebih rumit ekspresi, dan ini juga akan menghasilkan NULL sesekali:

SELECT [pilih] =CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');

IIF() adalah fungsi yang saya harapkan jatuh ke dalam perangkap yang sama, tetapi fungsi ini sebenarnya hanya CASE yang dicari ekspresi dengan hanya dua kemungkinan hasil, dan tanpa ELSE – jadi sulit, tanpa bersarang dan memperkenalkan fungsi lain, untuk membayangkan skenario di mana ini dapat rusak secara tak terduga. Sementara dalam kasus sederhana ini adalah singkatan yang layak untuk CASE , juga sulit untuk melakukan sesuatu yang berguna dengannya jika Anda membutuhkan lebih dari dua kemungkinan hasil. :-)

COALESCE() juga terpengaruh

Terakhir, kita harus memeriksa COALESCE dapat memiliki masalah serupa. Mari kita pertimbangkan bahwa ekspresi ini setara:

PILIH COALESCE(@variabel, 'konstan'); PILIH KASUS KETIKA @variabel BUKAN NULL MAKA @variable ELSE 'constant' END);

Dalam hal ini, @variable akan dievaluasi dua kali (seperti halnya fungsi atau subkueri apa pun, seperti yang dijelaskan dalam item Connect ini).

Saya benar-benar bisa mendapatkan beberapa pandangan bingung ketika saya membawa contoh berikut dalam diskusi forum baru-baru ini. Katakanlah saya ingin mengisi tabel dengan distribusi nilai dari 1-5, tetapi setiap kali 3 ditemukan, saya ingin menggunakan -1 sebagai gantinya. Bukan skenario dunia nyata, tetapi mudah dibuat dan diikuti. Salah satu cara untuk menulis ekspresi ini adalah:

PILIH COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);

(Dalam bahasa Inggris, bekerja dari dalam ke luar:ubah hasil ekspresi 1+RAND()*5 menjadi kecil; jika hasil konversinya adalah 3, setel ke NULL; jika hasilnya NULL , setel ke -1. Anda dapat menulis ini dengan CASE yang lebih verbose ekspresi, tapi singkat tampaknya menjadi raja.)

Jika Anda menjalankannya beberapa kali, Anda akan melihat rentang nilai dari 1-5, serta -1. Anda akan melihat beberapa contoh 3, dan Anda mungkin juga memperhatikan bahwa Anda kadang-kadang melihat NULL , meskipun Anda mungkin tidak mengharapkan salah satu dari hasil tersebut. Mari kita periksa distribusinya:

GUNAKAN tempdb;GOCREATE TABLE dbo.dist(TheNumber SMALLINT);GOINSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);GO 10000SELECT TheNumber, kejadian =COUNT(*) FROM dbo.dist GROUP BY TheNumber ORDER BY TheNumber;GODROP TABLE dbo.dist;

Hasil (hasil Anda pasti akan bervariasi, tetapi tren dasarnya harus serupa):

Nomor kejadian
NULL 1,654
-1 2.002
1 1.290
2 1.266
3 1.287
4 1.251
5 1.250

Distribusi TheNumber menggunakan COALESCE

Mengurai ekspresi CASE yang dicari

Apakah Anda menggaruk kepala Anda belum? Bagaimana nilai NULL dan 3 muncul, dan mengapa distribusi untuk NULL dan -1 jauh lebih tinggi? Yah, saya akan menjawab yang pertama secara langsung, dan mengundang hipotesis untuk yang terakhir.

Ekspresi secara kasar berkembang menjadi berikut, secara logis, karena RAND() dievaluasi dua kali di dalam NULLIF , lalu kalikan dengan dua evaluasi untuk setiap cabang COALESCE fungsi. Saya tidak memiliki debugger, jadi ini belum tentu *persis* apa yang dilakukan di dalam SQL Server, tetapi harus cukup setara untuk menjelaskan maksudnya:

PILIH KASUS KETIKA KASUS KETIKA CONVERT(SMALLINT,1+RAND()*5) =3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END BUKAN NULL KASUS KETIKA CONVERT(SMALLINT,1+ RAND()*5) =3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END ELSE -1 ENDEND

Jadi Anda dapat melihat bahwa dievaluasi beberapa kali dapat dengan cepat menjadi buku Choose Your Own Adventure™, dan bagaimana keduanya NULL dan 3 adalah hasil yang mungkin yang tampaknya tidak mungkin ketika memeriksa pernyataan asli. Catatan tambahan yang menarik:ini tidak terjadi sama jika Anda mengambil skrip distribusi di atas dan mengganti COALESCE dengan ISNULL . Dalam hal ini, tidak ada kemungkinan untuk NULL keluaran; distribusinya kira-kira sebagai berikut:

Nomor kejadian
-1 1,966
1 1,585
2 1,644
3 1.573
4 1,598
5 1,634

Distribusi TheNumber menggunakan ISNULL

Sekali lagi, hasil aktual Anda pasti akan bervariasi, tetapi seharusnya tidak terlalu banyak. Intinya adalah bahwa kita masih dapat melihat bahwa 3 sering jatuh melalui celah, tetapi ISNULL secara ajaib menghilangkan potensi NULL untuk menyelesaikannya.

Saya berbicara tentang beberapa perbedaan lain antara COALESCE dan ISNULL dalam tip, berjudul "Memutuskan antara COALESCE dan ISNULL di SQL Server." Ketika saya menulis itu, saya sangat mendukung penggunaan COALESCE kecuali dalam kasus di mana argumen pertama adalah subquery (sekali lagi, karena bug ini "celah fitur"). Sekarang saya tidak begitu yakin saya merasa kuat tentang itu.

Ekspresi CASE sederhana dapat menjadi bersarang di atas server yang ditautkan

Salah satu dari sedikit batasan CASE ekspresi adalah yang dibatasi hingga 10 level sarang. Dalam contoh ini di dba.stackexchange.com, Paul White mendemonstrasikan (menggunakan Plan Explorer) bahwa ekspresi sederhana seperti ini:

SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ...ENDFROM ...

Diperluas oleh pengurai ke formulir yang dicari:

PILIH KASUS KETIKA column_name ='1' THEN 'a' WHEN column_name ='2' THEN 'b' WHEN column_name ='3' THEN 'c' ...ENDFROM ...

Tetapi sebenarnya dapat ditransmisikan melalui koneksi server yang ditautkan sebagai berikut, permintaan yang jauh lebih bertele-tele:

SELECT CASE WHEN column_name ='1' THEN 'a' ELSE CASE WHEN column_name ='2' THEN 'b' ELSE CASE WHEN column_name ='3' THEN 'c' ELSE ... ELSE NULL END END ENDFROM .. .

Dalam situasi ini, meskipun kueri asli hanya memiliki satu CASE ekspresi dengan 10+ kemungkinan hasil, ketika dikirim ke server tertaut, itu memiliki 10+ bersarang CASE ekspresi. Dengan demikian, seperti yang Anda duga, itu mengembalikan kesalahan:

Msg 8180, Level 16, State 1
Pernyataan tidak dapat disiapkan.
Msg 125, Level 15, State 4
Ekspresi kasus hanya dapat disarangkan ke level 10.

Dalam beberapa kasus, Anda dapat menulis ulang seperti yang disarankan Paul, dengan ekspresi seperti ini (dengan asumsi column_name adalah kolom varchar):

SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ...ENDFROM . ..

Dalam beberapa kasus, hanya SUBSTRING mungkin diperlukan untuk mengubah lokasi di mana ekspresi dievaluasi; di tempat lain, hanya CONVERT . Saya tidak melakukan pengujian menyeluruh, tetapi ini mungkin berkaitan dengan penyedia server tertaut, opsi seperti Collation Compatible dan Use Remote Collation, dan versi SQL Server di kedua ujung pipa.

Singkat cerita, penting untuk diingat bahwa CASE . Anda ekspresi dapat ditulis ulang untuk Anda tanpa peringatan, dan bahwa solusi apa pun yang Anda gunakan nanti dapat ditolak oleh pengoptimal, meskipun itu bekerja untuk Anda sekarang.

Pemikiran Akhir Ekspresi KASUS dan Sumber Daya Tambahan

Saya harap saya telah memberikan beberapa pemikiran untuk beberapa aspek yang kurang dikenal dari CASE ekspresi, dan beberapa wawasan tentang situasi di mana CASE – dan beberapa fungsi yang menggunakan logika dasar yang sama – mengembalikan hasil yang tidak terduga. Beberapa skenario menarik lainnya di mana jenis masalah ini muncul:

  • Stack Overflow :Bagaimana ekspresi CASE ini mencapai klausa ELSE?
  • Stack Overflow :CRYPT_GEN_RANDOM() Efek Aneh
  • Stack Overflow :CHOOSE() Tidak Berfungsi seperti yang Dimaksud
  • Stack Overflow :CHECKSUM(NewId()) dijalankan beberapa kali per baris
  • Hubungkan #350485 :Bug dengan NEWID() dan Ekspresi Tabel

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Penyembunyian Data dalam Aplikasi DB

  2. Parsing nilai default parameter menggunakan PowerShell – Bagian 1

  3. Menggunakan Pola Alur Kerja untuk Mengelola Status Entitas Apa Pun

  4. Pertanyaan Wawancara Insinyur Data Dengan Python

  5. Bagaimana Menjumlahkan Nilai Kolom dalam SQL?