Saya sudah sering membaca ketika seseorang harus memeriksa keberadaan sebuah baris harus selalu dilakukan dengan EXIS bukan dengan COUNT.
Sangat jarang hal apa pun selalu benar, terutama ketika menyangkut database. Ada sejumlah cara untuk mengekspresikan semantik yang sama dalam SQL. Jika ada aturan praktis yang berguna, mungkin untuk menulis pertanyaan menggunakan sintaksis paling alami yang tersedia (dan, ya, itu subjektif) dan hanya mempertimbangkan penulisan ulang jika rencana kueri atau kinerja yang Anda dapatkan tidak dapat diterima.
Untuk apa nilainya, pendapat saya sendiri tentang masalah ini adalah bahwa pertanyaan keberadaan secara alami diekspresikan menggunakan EXISTS
. Ini juga pengalaman saya yang EXISTS
cenderung mengoptimalkan lebih baik daripada alternatif OUTER JOIN
tolak NULL
. Menggunakan COUNT(*)
dan memfilter =0
adalah alternatif lain, yang kebetulan memiliki beberapa dukungan dalam pengoptimal permintaan SQL Server, tapi saya pribadi menemukan ini tidak dapat diandalkan dalam permintaan yang lebih kompleks. Bagaimanapun, EXISTS
sepertinya jauh lebih alami (bagi saya) daripada salah satu dari alternatif tersebut.
Saya bertanya-tanya apakah ada cacat yang tidak diketahui dengan EXIS yang memberikan rasa yang sempurna untuk pengukuran yang telah saya lakukan
Contoh khusus Anda menarik, karena menyoroti cara pengoptimal berurusan dengan subkueri dalam CASE
ekspresi (dan EXISTS
tes khususnya).
Subqueries dalam ekspresi CASE
Pertimbangkan permintaan berikut (legal):
DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);
SELECT
CASE
WHEN (SELECT W.b FROM @When AS W) = 1
THEN (SELECT T.c FROM @Then AS T)
ELSE (SELECT E.d FROM @Else AS E)
END
FROM @Base AS B;
The semantikCASE
adalah bahwa WHEN/ELSE
klausul yang umumnya dievaluasi dalam rangka tekstual. Dalam kueri di atas, SQL Server akan salah untuk mengembalikan kesalahan jika ELSE
subquery mengembalikan lebih dari satu baris, jika WHEN
klausa terpenuhi. Untuk menghormati semantik ini, pengoptimal menghasilkan rencana yang menggunakan predikat pass-through:
Sisi dalam dari nested loop bergabung hanya dievaluasi ketika predikat pass-through mengembalikan false. Efek keseluruhannya adalah CASE
ekspresi diuji secara berurutan, dan subkueri hanya dievaluasi jika tidak ada ekspresi sebelumnya yang dipenuhi.
Ekspresi CASE dengan subquery EXISTS
Di mana CASE
subquery digunakan EXISTS
, tes keberadaan logis diimplementasikan sebagai semi-join, tetapi baris yang biasanya akan ditolak oleh semi-join harus dipertahankan jika klausa nanti membutuhkannya. Baris yang mengalir melalui semi-join jenis khusus ini memperoleh bendera untuk menunjukkan apakah semi-join menemukan kecocokan atau tidak. Bendera ini dikenal sebagai kolom probe .
Detail implementasi adalah bahwa subquery logis digantikan oleh gabungan berkorelasi ('berlaku') dengan kolom probe. Pekerjaan dilakukan oleh aturan penyederhanaan dalam optimizer kueri yang disebut RemoveSubqInPrj
(hapus subquery dalam proyeksi). Kita dapat melihat detail menggunakan jejak bendera 8606:
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);
Bagian dari pohon input yang menunjukkan EXISTS
tes ditunjukkan di bawah ini:
ScaOp_Exists
LogOp_Project
LogOp_Select
LogOp_Get TBL: #T2
ScaOp_Comp x_cmpEq
ScaOp_Identifier [T2].ID
ScaOp_Identifier [T1].ID
Ini ditransformasikan oleh RemoveSubqInPrj
ke struktur dipimpin oleh:
LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)
Ini adalah semi-gabung kiri berlaku dengan probe yang dijelaskan sebelumnya. Transformasi awal ini adalah satu-satunya yang tersedia di pengoptimal permintaan SQL Server hingga saat ini, dan kompilasi hanya akan gagal jika transformasi ini dinonaktifkan.
Salah satu bentuk rencana eksekusi yang mungkin untuk kueri ini adalah implementasi langsung dari struktur logis itu:
Scalar Hitung akhir mengevaluasi hasil CASE
ekspresi menggunakan nilai kolom probe:
Bentuk dasar pohon rencana dipertahankan ketika optimasi mempertimbangkan jenis penggabungan fisik lainnya untuk gabung semi. Hanya penggabungan gabung yang mendukung kolom probe, jadi gabung semi hash, meskipun secara logis mungkin, tidak dipertimbangkan:
Perhatikan hasil gabungan ekspresi berlabel Expr1008
(bahwa nama adalah sama seperti sebelumnya adalah kebetulan) meskipun tidak ada definisi untuk itu muncul pada operator mana pun dalam paket. Ini hanya kolom probe lagi. Seperti sebelumnya, Compute Scalar akhir menggunakan nilai probe ini untuk mengevaluasi CASE
.
Masalahnya adalah bahwa pengoptimal tidak sepenuhnya mengeksplorasi alternatif yang hanya menjadi bermanfaat dengan menggabungkan (atau hash) setengah bergabung. Dalam rencana loop bersarang, tidak ada keuntungan untuk memeriksa apakah baris T2
cocok dengan rentang pada setiap iterasi. Dengan rencana penggabungan atau hash, ini bisa menjadi optimasi yang berguna.
Jika kami menambahkan BETWEEN
predikat yang cocok ke T2
dalam kueri, yang terjadi adalah pemeriksaan ini dilakukan untuk setiap baris sebagai residu pada gabungan gabungan (sulit dikenali dalam rencana eksekusi, tetapi ada di sana):
SELECT
T1.ID,
CASE
WHEN EXISTS
(
SELECT 1
FROM #T2 AS T2
WHERE T2.ID = T1.ID
AND T2.ID BETWEEN 5000 AND 7000 -- New
) THEN 1
ELSE 0
END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Kami berharap bahwa BETWEEN
predikat itu malah akan didorong ke bawah untuk T2
menghasilkan pencarian. Biasanya, pengoptimal akan mempertimbangkan untuk melakukan ini (bahkan tanpa predikat tambahan dalam kueri). Ini mengakui predikat tersirat ( BETWEEN
pada T1
dan gabungan predikat antara T1
dan T2
bersama - sama menyiratkan BETWEEN
on T2
) tanpa mereka ada dalam teks kueri asli. Sayangnya, pola apply-probe berarti ini tidak dieksplorasi.
Ada cara untuk menulis kueri untuk menghasilkan pencarian pada kedua input ke gabungan gabungan. Salah satu cara melibatkan penulisan kueri dengan cara yang tidak wajar (mengalahkan alasan yang biasanya saya sukai EXISTS
):
WITH T2 AS
(
SELECT TOP (9223372036854775807) *
FROM #T2 AS T2
WHERE ID BETWEEN 5000 AND 7000
)
SELECT
T1.ID,
DoesExist =
CASE
WHEN EXISTS
(
SELECT * FROM T2
WHERE T2.ID = T1.ID
) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;
Saya tidak akan senang menulis permintaan itu di lingkungan produksi, itu hanya untuk menunjukkan bahwa bentuk rencana yang diinginkan adalah mungkin. Jika kueri sebenarnya yang Anda butuhkan untuk menulis menggunakan CASE
cara khusus ini, dan kinerja menderita karena tidak ada pencarian di sisi penyelidikan semi-gabungan gabungan, Anda dapat mempertimbangkan untuk menulis kueri menggunakan sintaks berbeda yang menghasilkan hasil yang tepat dan rencana pelaksanaan yang lebih efisien.