Apakah SQL Server membaca semua fungsi COALESCE bahkan jika argumen pertama bukan NULL?


98

Saya menggunakan fungsi T-SQL di COALESCEmana argumen pertama tidak akan menjadi nol pada sekitar 95% kali dijalankan. Jika argumen pertama adalah NULL, argumen kedua adalah proses yang cukup panjang:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

Jika, misalnya, c.FirstName = 'John'apakah SQL Server masih menjalankan sub-kueri?

Saya tahu dengan IIF()fungsi VB.NET , jika argumen kedua Benar, kode masih membaca argumen ketiga (meskipun tidak akan digunakan).

Jawaban:


95

Tidak . Inilah tes sederhana:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

Jika kondisi kedua dievaluasi, pengecualian dilemparkan untuk divide-by-zero.

Per Dokumentasi MSDN ini terkait dengan bagaimana COALESCEdilihat oleh penerjemah - itu hanya cara mudah untuk menulis CASEpernyataan.

CASE dikenal sebagai satu-satunya fungsi di SQL Server yang (kebanyakan) andal pendek.

Ada beberapa pengecualian ketika membandingkan dengan variabel skalar dan agregasi seperti yang ditunjukkan oleh Aaron Bertrand dalam jawaban lain di sini (dan ini akan berlaku untuk CASEdan COALESCE):

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

akan menghasilkan pembagian dengan kesalahan nol.

Ini harus dianggap sebagai bug, dan sebagai aturan COALESCEakan diuraikan dari kiri ke kanan.


6
@JNK tolong lihat jawaban saya untuk melihat kasus yang sangat sederhana di mana ini tidak berlaku (kekhawatiran saya adalah bahwa ada lebih banyak lagi, skenario yang belum ditemukan - membuatnya sulit untuk disepakati yang CASEselalu mengevaluasi sirkuit dari kiri ke kanan dan selalu pendek) ).
Aaron Bertrand

4
@SQLKiwi perilaku menarik lainnya menunjuk saya ke: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- ulangi beberapa kali. NULLTerkadang Anda akan mendapatkannya . Coba lagi dengan ISNULL- Anda tidak akan pernah NULL...
Aaron Bertrand


@ Martin ya saya percaya begitu. Tapi bukan perilaku yang sebagian besar pengguna akan menemukan intuitif kecuali mereka pernah mendengar (atau digigit) masalah itu.
Aaron Bertrand

73

Bagaimana dengan yang ini - seperti yang dilaporkan kepada saya oleh Itzik Ben-Gan, yang diberitahu tentang itu oleh Jaime Lafargue ?

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

Hasil:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.

Tentu saja ada penyelesaian yang sepele, tetapi intinya tetap CASEsaja tidak selalu menjamin evaluasi dari arus pendek ke kanan / hubungan arus pendek. Saya melaporkan bug di sini dan ditutup sebagai "sesuai desain." Paul White kemudian mengajukan item Connect ini , dan ditutup sebagai Fixed. Bukan karena itu diperbaiki sendiri, tetapi karena mereka memperbarui Buku Online dengan deskripsi yang lebih akurat dari skenario di mana agregat dapat mengubah urutan evaluasi CASEekspresi. Baru-baru ini saya membuat blog lebih banyak tentang ini di sini .

EDIT hanya sebuah tambahan, sementara saya setuju bahwa ini adalah kasus tepi, bahwa sebagian besar waktu Anda dapat mengandalkan evaluasi kiri-ke-kanan dan hubungan arus pendek, dan bahwa ini adalah bug yang bertentangan dengan dokumentasi dan mungkin pada akhirnya akan diperbaiki ( ini tidak pasti - lihat percakapan tindak lanjut di posting blog Bart Duncan untuk mengetahui alasannya), saya harus tidak setuju ketika orang mengatakan bahwa sesuatu selalu benar bahkan jika ada kasus tepi tunggal yang membantahnya. Jika Itzik dan yang lainnya dapat menemukan bug soliter seperti ini, itu membuatnya setidaknya di bidang kemungkinan ada bug lain juga. Dan karena kita tidak tahu sisa dari kueri OP, kita tidak bisa mengatakan dengan pasti bahwa dia akan bergantung pada hubungan arus pendek ini tetapi akhirnya digigit olehnya. Jadi bagi saya, jawaban yang lebih aman adalah:

Meskipun Anda biasanya dapat mengandalkan CASEuntuk mengevaluasi dari kiri ke kanan dan korsleting, seperti dijelaskan dalam dokumentasi, itu tidak akurat untuk mengatakan bahwa Anda selalu dapat melakukannya. Ada dua kasus yang ditunjukkan pada halaman ini di mana itu tidak benar, dan tidak ada bug yang diperbaiki dalam versi SQL Server yang tersedia untuk umum.

EDIT di sini adalah kasus lain (saya harus berhenti melakukan itu) di mana CASEekspresi tidak mengevaluasi dalam urutan yang Anda harapkan, meskipun tidak ada agregat yang terlibat.


2
Dan sepertinya ada masalah lain dengan CASE yang diam-diam diperbaiki
Martin Smith

IMO ini tidak membuktikan bahwa evaluasi ekspresi CASE tidak dijamin karena nilai agregat dihitung sebelum dipilih (sehingga mereka dapat digunakan di dalam memiliki).
Salman A

1
@SalmanA Saya tidak yakin apa lagi yang mungkin dilakukan kecuali membuktikan dengan tepat bahwa urutan evaluasi dalam ekspresi KASUS tidak dijamin. Kami mendapatkan pengecualian karena agregat dihitung terlebih dahulu, meskipun itu dalam klausa ELSE yang - jika Anda membaca dokumentasi - tidak boleh dijangkau.
Aaron Bertrand

Agregat @AaronBertrand dihitung sebelum pernyataan KASUS (dan mereka harusnya IMO). Dokumentasi yang direvisi menunjukkan dengan tepat ini, bahwa kesalahan terjadi sebelum KASUS dievaluasi.
Salman A

@SalmanA Masih menunjukkan kepada pengembang kasual bahwa ekspresi CASE tidak mengevaluasi dalam urutan yang ditulis - mekanika yang mendasari tidak relevan jika semua yang Anda coba lakukan adalah memahami mengapa kesalahan datang dari cabang KASUS yang seharusnya tidak ' t telah tercapai. Apakah Anda memiliki argumen terhadap semua contoh lain di halaman ini juga?
Aaron Bertrand

37

Pandangan saya tentang ini adalah bahwa dokumentasi membuatnya cukup jelas bahwa tujuannya adalah bahwa KASUS harus mengalami hubungan pendek. Seperti yang disebutkan Harun, ada beberapa kasus (ha!) Di mana ini terbukti tidak selalu benar.

Sejauh ini, semua ini telah diakui sebagai bug dan diperbaiki - meskipun belum tentu dalam versi SQL Server Anda dapat membeli dan menambal hari ini (bug pelipatan konstan belum sampai ke Pembaruan Kumulatif AFAIK). Potensi bug terbaru - awalnya dilaporkan oleh Itzik Ben-Gan - belum diselidiki (baik Harun atau saya akan menambahkannya ke Connect segera).

Terkait dengan pertanyaan awal, ada masalah lain dengan KASUS (dan karena itu COALESCE) di mana fungsi efek samping atau sub-kueri digunakan. Mempertimbangkan:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);

Formulir COALESCE sering mengembalikan NULL, rincian lebih lanjut di https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

Masalah yang diperlihatkan dengan transformasi pengoptimal dan pelacakan persamaan ekspresi berarti bahwa tidak mungkin untuk menjamin bahwa CASE akan mengalami hubungan arus pendek dalam semua keadaan. Saya dapat membayangkan kasus-kasus di mana bahkan tidak mungkin untuk memprediksi perilaku dengan memeriksa output rencana acara publik, meskipun saya tidak memiliki repro untuk itu hari ini.

Singkatnya, saya pikir Anda bisa cukup yakin bahwa CASE akan mengalami hubungan pendek secara umum (terutama jika orang yang cukup terampil memeriksa rencana eksekusi, dan bahwa rencana eksekusi 'ditegakkan' dengan panduan atau petunjuk rencana) tetapi jika Anda perlu jaminan mutlak, Anda harus menulis SQL yang tidak termasuk ekspresi sama sekali.

Kurasa bukan urusan yang sangat memuaskan.


18

Aku datang di kasus lain di mana CASE/ COALESCElakukan sirkuit tidak singkat. TVF berikut akan meningkatkan pelanggaran PK jika disahkan 1sebagai parameter.

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END

Jika dipanggil sebagai berikut

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 

Atau sebagai

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

Keduanya memberikan hasil

Pelanggaran terhadap batasan utama PRIMARY 'PK__F__3BD019A800551192'. Tidak dapat memasukkan kunci duplikat di objek 'dbo. @ T'. Nilai kunci duplikat adalah (1).

menunjukkan bahwa SELECT(atau setidaknya populasi variabel tabel) masih dilakukan dan menimbulkan kesalahan meskipun cabang pernyataan itu tidak boleh dihubungi. Rencana untuk COALESCEversi di bawah.

Rencana

Penulisan ulang kueri ini muncul untuk menghindari masalah

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   

Yang memberi rencana

Plan2


8

Contoh lain

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);

Kueri

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)

Tidak menunjukkan pembacaan T2sama sekali.

Pencarian T2sedang dalam predikat lulus dan operator tidak pernah dieksekusi. Tapi

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)

Apakah acara itu T2dibaca. Meskipun tidak ada nilai dari T2yang benar-benar dibutuhkan.

Tentu saja ini tidak benar-benar mengejutkan tetapi saya pikir layak menambahkan ke repositori contoh counter jika hanya karena menimbulkan masalah apa hubungan pendek bahkan berarti dalam bahasa deklaratif berdasarkan set.


7

Saya hanya ingin menyebutkan strategi yang mungkin tidak Anda pertimbangkan. Ini mungkin bukan pertandingan di sini, tetapi kadang-kadang berguna. Lihat apakah modifikasi ini memberi Anda kinerja yang lebih baik:

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )

Cara lain untuk melakukannya adalah ini (pada dasarnya setara, tetapi memungkinkan Anda untuk mengakses lebih banyak kolom dari permintaan lain jika perlu):

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x

Pada dasarnya ini adalah teknik "keras" bergabung dengan tabel tetapi termasuk kondisi kapan setiap baris harus BERGABUNG. Dalam pengalaman saya, ini benar-benar membantu rencana eksekusi.


3

Tidak, itu tidak akan terjadi. Itu hanya akan berjalan saat c.FirstNameini NULL.

Namun, Anda harus mencobanya sendiri. Percobaan. Anda bilang subquery Anda panjang. Tolok ukur. Buat kesimpulan sendiri tentang ini.

@ Harun jawaban pada sub-kueri yang dijalankan lebih lengkap.

Namun, saya masih berpikir Anda harus mengerjakan ulang permintaan dan penggunaan Anda LEFT JOIN. Sebagian besar waktu, sub kueri dapat dihapus dengan mengerjakan ulang kueri Anda untuk menggunakan LEFT JOINs.

Masalah dengan menggunakan sub kueri adalah bahwa keseluruhan pernyataan Anda akan berjalan lebih lambat karena sub kueri dijalankan untuk setiap baris di set hasil kueri utama.


@Adrian itu masih tidak benar. Lihatlah rencana eksekusi dan Anda akan melihat bahwa subqueries sering dikonversi dengan cukup cerdas menjadi BERGABUNG. Ini adalah kesalahan eksperimen-pikiran belaka untuk mengasumsikan bahwa seluruh subquery harus dijalankan berulang-ulang untuk setiap baris, meskipun ini dapat secara efektif terjadi jika loop bersarang bergabung dengan pemindaian dipilih.
ErikE

3

Standar aktual mengatakan bahwa semua klausa KETIKA (serta klausa ELSE) harus diuraikan untuk menentukan tipe data ekspresi secara keseluruhan. Saya benar-benar harus mengeluarkan beberapa catatan lama saya untuk menentukan bagaimana kesalahan ditangani. Tapi begitu saja, 1/0 menggunakan bilangan bulat, jadi saya akan berasumsi bahwa itu kesalahan. Ini kesalahan dengan tipe data integer. Ketika Anda hanya memiliki nol dalam daftar penyatuan, agak sulit untuk menentukan tipe data, dan itu masalah lain.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.