urutan klausa dalam "EXISTS (...) ATAU EXISTS (...)"

11

Saya memiliki kelas pertanyaan yang menguji keberadaan satu dari dua hal. Itu adalah bentuk

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

Pernyataan aktual dihasilkan dalam C dan dieksekusi sebagai permintaan ad-hoc melalui koneksi ODBC.

Baru-baru ini terungkap bahwa SELECT kedua mungkin akan lebih cepat daripada SELECT pertama dalam banyak kasus dan bahwa mengganti urutan kedua klausa EXISTS menyebabkan percepatan yang drastis dalam setidaknya satu kasus uji kasar yang baru saja kita buat.

Hal yang jelas untuk dilakukan adalah langsung beralih dua klausa, tapi saya ingin melihat apakah seseorang yang lebih akrab dengan SQL Server akan peduli untuk mempertimbangkan ini. Rasanya seperti saya mengandalkan kebetulan dan "detail implementasi".

(Sepertinya SQL Server lebih pintar, itu akan mengeksekusi kedua klausa EXISTS secara paralel dan membiarkan salah satu yang pertama menyelesaikan hubungan pendek yang lain.)

Apakah ada cara yang lebih baik untuk mendapatkan SQL Server untuk secara konsisten meningkatkan waktu berjalan dari permintaan seperti itu?

Memperbarui

Terima kasih atas waktu dan minat Anda pada pertanyaan saya. Saya tidak mengharapkan pertanyaan tentang rencana kueri yang sebenarnya, tetapi saya bersedia membagikannya.

Ini untuk komponen perangkat lunak yang mendukung SQL Server 2008R2 dan lebih tinggi. Bentuk data bisa sangat berbeda tergantung pada konfigurasi dan penggunaan. Rekan kerja saya berpikir untuk melakukan perubahan pada kueri ini karena (dalam contoh) dbf_1162761$z$rv$1257927703tabel akan selalu memiliki lebih besar atau sama dengan jumlah baris di dalamnya daripada dbf_1162761$z$dd$1257927703tabel - kadang-kadang secara signifikan lebih banyak (urutan besarnya).

Ini kasus kasar yang saya sebutkan. Permintaan pertama adalah yang lambat dan memakan waktu sekitar 20 detik. Kueri kedua selesai dalam sekejap.

Untuk apa nilainya, bit "MENGOPTIMALKAN UNTUK TIDAK DIKETAHUI" juga ditambahkan baru-baru ini karena parameter sniffing merusak kasus tertentu.

Permintaan asli:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Rencana asli:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

Memperbaiki kueri:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Rencana tetap:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
jr
sumber

Jawaban:

11

Sebagai aturan umum, SQL Server akan mengeksekusi bagian-bagian dari CASEpernyataan secara berurutan tetapi bebas untuk menyusun ulang ORkondisi. Untuk beberapa kueri, Anda bisa mendapatkan kinerja yang lebih baik secara konsisten dengan mengubah urutan WHENekspresi di dalam CASEpernyataan. Terkadang Anda juga bisa mendapatkan kinerja yang lebih baik ketika mengubah urutan kondisi dalam sebuah ORpernyataan, tetapi itu bukan perilaku yang dijamin.

Mungkin lebih baik untuk menjalaninya dengan contoh sederhana. Saya sedang menguji terhadap SQL Server 2016 sehingga mungkin Anda tidak akan mendapatkan hasil yang sama persis di komputer Anda, tetapi sejauh yang saya tahu prinsip yang sama berlaku. Pertama saya akan menempatkan satu juta bilangan bulat dari 1 hingga 10.000 di dua tabel, satu dengan indeks berkerumun dan satu sebagai tumpukan:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Pertimbangkan pertanyaan berikut:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Kita tahu bahwa mengevaluasi subquery terhadap X_CIakan jauh lebih murah daripada subquery terhadap X_HEAP, terutama ketika tidak ada baris yang cocok. Jika tidak ada baris yang cocok maka kita hanya perlu melakukan beberapa pembacaan logis terhadap tabel dengan indeks berkerumun. Namun, kita perlu memindai semua baris tumpukan untuk mengetahui bahwa tidak ada baris yang cocok. Pengoptimal juga mengetahui hal ini. Secara umum, menggunakan indeks berkerumun untuk mencari satu baris sangat murah dibandingkan dengan memindai tabel.

Untuk contoh data ini saya akan menulis kueri seperti ini:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Itu secara efektif memaksa SQL Server untuk menjalankan subquery terhadap tabel dengan indeks berkerumun terlebih dahulu. Inilah hasil dari SET STATISTICS IO, TIME ON:

Tabel 'X_CI'. Pindai hitungan 0, bacaan logis 3, bacaan fisik 0

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

Melihat rencana permintaan, jika pencarian di label 1 mengembalikan data apa pun selain pemindaian di label 2 tidak diperlukan dan tidak akan terjadi:

pertanyaan yang bagus

Permintaan berikut ini jauh lebih efisien:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

Melihat rencana permintaan, kami melihat bahwa pemindaian pada label 2 selalu terjadi. Jika baris ditemukan maka pencarian di label 1 dilewati. Itu bukan urutan yang kami inginkan:

rencana kueri yang buruk

Hasil kinerja kembali yang naik:

Tabel 'X_HEAP'. Pindai hitungan 1, bacaan logis 7247

Waktu Eksekusi SQL Server: Waktu CPU = 15 ms, waktu yang berlalu = 22 ms.

Kembali ke permintaan asli, untuk permintaan ini saya melihat pencarian dan pemindaian dievaluasi dalam urutan yang baik untuk kinerja:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Dan dalam kueri ini mereka dievaluasi dalam urutan yang berlawanan:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

Namun, tidak seperti pasangan kueri sebelumnya, tidak ada yang memaksa pengoptimal permintaan SQL Server untuk mengevaluasi satu sebelum yang lain. Anda seharusnya tidak mengandalkan perilaku itu untuk hal-hal penting.

Kesimpulannya, jika Anda memerlukan satu subquery untuk dievaluasi sebelum yang lain maka gunakan CASEpernyataan atau metode lain untuk memaksa pemesanan. Jika tidak, silakan memesan subquery dalam ORkondisi sesuai keinginan Anda, tetapi ketahuilah bahwa tidak ada jaminan bahwa pengoptimal akan menjalankannya dalam urutan seperti yang tertulis.

Tambahan:

Pertanyaan tindak lanjut alami adalah apa yang dapat Anda lakukan jika Anda ingin SQL Server untuk memutuskan permintaan mana yang lebih murah dan untuk mengeksekusi yang pertama? Semua metode sejauh ini tampaknya diimplementasikan oleh SQL Server dalam urutan permintaan ditulis, bahkan jika itu tidak dijamin perilaku untuk sebagian dari mereka.

Berikut adalah salah satu opsi yang tampaknya berfungsi untuk tabel demo sederhana:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Anda dapat menemukan demo biola db di sini . Mengubah urutan tabel yang diturunkan tidak mengubah rencana kueri. Di kedua kueri X_HEAPtabel tidak tersentuh. Dengan kata lain, pengoptimal permintaan muncul untuk menjalankan permintaan yang lebih murah terlebih dahulu. Saya tidak bisa merekomendasikan menggunakan sesuatu seperti ini dalam produksi jadi ini di sini untuk sebagian besar nilai rasa ingin tahu Mungkin ada cara yang jauh lebih sederhana untuk mencapai hal yang sama.

Joe Obbish
sumber
4
Atau CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDbisa menjadi alternatif, meskipun itu masih bergantung pada manual memutuskan permintaan mana yang lebih cepat dan menempatkan yang pertama. Saya tidak yakin apakah ada cara untuk mengekspresikannya sehingga SQL Server akan secara otomatis memesan ulang sehingga yang murah secara otomatis dievaluasi terlebih dahulu.
Martin Smith