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 1Ditemukan 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 untukWHEN
saat ini , dan akhirnya tekanELSE
ayat. Untuk melindungi diri Anda dari hal ini, satu opsi adalah selalu membuat kode kerasELSE
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 mengubahWHEN
terakhir klausa keELSE
, 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 denganRAND()
ekspresi, Anda akan mendapatkan hasil yang sama sekali berbeda; terutama, yang terakhir hanya akan mengembalikan satu nilai. Ini karenaRAND()
, sepertiGETDATE()
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 mengembalikanNULL
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 pencarianCASE
yang lebih rumit ekspresi, dan ini juga akan menghasilkanNULL
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 hanyaCASE
yang dicari ekspresi dengan hanya dua kemungkinan hasil, dan tanpaELSE
– 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 untukCASE
, 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 keNULL
; jika hasilnyaNULL
, setel ke -1. Anda dapat menulis ini denganCASE
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 untukNULL
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 dalamNULLIF
, lalu kalikan dengan dua evaluasi untuk setiap cabangCOALESCE
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 ENDENDJadi 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 menggantiCOALESCE
denganISNULL
. Dalam hal ini, tidak ada kemungkinan untukNULL
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 potensiNULL
untuk menyelesaikannya.Saya berbicara tentang beberapa perbedaan lain antara
COALESCE
danISNULL
dalam tip, berjudul "Memutuskan antara COALESCE dan ISNULL di SQL Server." Ketika saya menulis itu, saya sangat mendukung penggunaanCOALESCE
kecuali dalam kasus di mana argumen pertama adalah subquery (sekali lagi, karenabugini "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
Msg 8180, Level 16, State 1CASE
ekspresi dengan 10+ kemungkinan hasil, ketika dikirim ke server tertaut, itu memiliki 10+ bersarangCASE
ekspresi. Dengan demikian, seperti yang Anda duga, itu mengembalikan kesalahan:
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, hanyaCONVERT
. 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 manaCASE
– 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