Periksa keberadaan dengan EXIS yang mengungguli COUNT! ... tidak?

35

Saya sudah sering membaca ketika seseorang harus memeriksa keberadaan sebuah baris harus selalu dilakukan dengan EXIS bukan dengan COUNT.

Namun dalam beberapa skenario terakhir saya telah mengukur peningkatan kinerja saat menggunakan hitungan.
Polanya seperti ini:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

Saya tidak terbiasa dengan metode untuk mengetahui apa yang terjadi "di dalam" SQL Server jadi saya bertanya-tanya apakah ada cacat yang tidak diketahui dengan EXIS yang memberikan arti yang masuk akal untuk pengukuran yang telah saya lakukan (dapatkah EXISTS menjadi RBAR ?!).

Apakah Anda punya penjelasan tentang fenomena itu?

EDIT:

Berikut ini skrip lengkap yang dapat Anda jalankan:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

Pada SQL Server 2008R2 (Seven 64bits) saya mendapatkan hasil ini

COUNT Versi:

Tabel '# 455F344D'. Pindai hitungan 1, bacaan logis 8, bacaan fisik 0, bacaan baca-depan 0, bacaan logis lob 0, bacaan fisik lob 0, bacaan baca lob depan 0.
Tabel '# 492FC531'. Pindai hitungan 1, bacaan logis 30, bacaan fisik 0, bacaan baca depan 0, bacaan logis lob 0, bacaan fisik lob 0, bacaan baca lob depan 0.

Waktu Eksekusi SQL Server:
Waktu CPU = 0 ms, waktu yang berlalu = 81 ms.

EXISTS Versi:

Tabel '# 492FC531'. Pindai hitungan 1, bacaan logis 96, bacaan fisik 0, bacaan baca depan 0, bacaan logis lob 0, bacaan fisik lob 0, bacaan baca lob depan 0.
Tabel '# 455F344D'. Pindai hitungan 1, bacaan logis 8, bacaan fisik 0, bacaan baca depan 0, bacaan logis lob 0, bacaan fisik lob 0, bacaan baca lob depan 0.

Waktu Eksekusi SQL Server:
Waktu CPU = 0 ms, waktu yang berlalu = 76 ms.


sumber

Jawaban:

43

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 JOINtolak NULL. Menggunakan COUNT(*)dan memfilter =0adalah 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, EXISTSsepertinya 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 CASEekspresi (dan EXISTStes 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/ELSEklausul yang umumnya dievaluasi dalam rangka tekstual. Dalam kueri di atas, SQL Server akan salah untuk mengembalikan kesalahan jika ELSEsubquery mengembalikan lebih dari satu baris, jika WHENklausa terpenuhi. Untuk menghormati semantik ini, pengoptimal menghasilkan rencana yang menggunakan predikat pass-through:

Predikat pass-through

Sisi dalam dari nested loop bergabung hanya dievaluasi ketika predikat pass-through mengembalikan false. Efek keseluruhannya adalah CASEekspresi diuji secara berurutan, dan subkueri hanya dievaluasi jika tidak ada ekspresi sebelumnya yang dipenuhi.

Ekspresi CASE dengan subquery EXISTS

Di mana CASEsubquery 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 EXISTStes 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 RemoveSubqInPrjke 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:

NLJ Semi Gabung dengan Probe

Scalar Hitung akhir mengevaluasi hasil CASEekspresi menggunakan nilai kolom probe:

Hitung ekspresi Skalar

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:

Gabungkan dengan kolom probe

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 T2cocok dengan rentang pada setiap iterasi. Dengan rencana penggabungan atau hash, ini bisa menjadi optimasi yang berguna.

Jika kami menambahkan BETWEENpredikat yang cocok ke T2dalam 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;

Predikat residual

Kami berharap bahwa BETWEENpredikat itu malah akan didorong ke bawah untuk T2menghasilkan pencarian. Biasanya, pengoptimal akan mempertimbangkan untuk melakukan ini (bahkan tanpa predikat tambahan dalam kueri). Ini mengakui predikat tersirat ( BETWEENpada T1dan gabungan predikat antara T1dan T2bersama - sama menyiratkan BETWEENon 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;

Rencana trik TOP

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 CASEcara 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.

Paul White mengatakan GoFundMonica
sumber
6

The "COUNT (*) vs ADA" argumen adalah melakukan dengan memeriksa apakah catatan ada. Sebagai contoh:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

vs.

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

Skrip SQL Anda tidak menggunakan COUNT(*)sebagai catatan ada cek, dan karena itu saya tidak akan mengatakan bahwa itu berlaku dalam skenario Anda.

Singkat
sumber
Adakah / kesimpulan berdasarkan script yang saya posting?