Mengapa CTE rekursif ini dengan parameter tidak menggunakan indeks ketika itu dilakukan dengan literal?

8

Saya menggunakan CTE rekursif pada struktur pohon untuk mendaftar semua keturunan dari simpul tertentu di pohon. Jika saya menulis nilai simpul literal dalam WHEREklausa saya , SQL Server tampaknya benar-benar menerapkan CTE hanya untuk nilai itu, memberikan rencana kueri dengan jumlah baris aktual yang rendah, dan lain-lain :

rencana permintaan dengan nilai literal

Namun, jika saya meneruskan nilai sebagai parameter, tampaknya menyadari (spool) CTE dan kemudian memfilternya setelah fakta :

paket permintaan dengan nilai parameter

Saya bisa salah membaca rencana. Saya belum melihat masalah kinerja, tetapi saya khawatir bahwa realisasi CTE dapat menyebabkan masalah dengan kumpulan data yang lebih besar, terutama dalam sistem yang lebih sibuk. Juga, saya biasanya memperumit lintasan ini pada dirinya sendiri: Saya melintasi leluhur dan kembali ke keturunan (untuk memastikan bahwa saya mengumpulkan semua simpul terkait). Karena bagaimana data saya, setiap set node "terkait" agak kecil, sehingga realisasi CTE tidak masuk akal. Dan ketika SQL Server tampaknya menyadari CTE, itu memberi saya beberapa angka yang cukup besar dalam jumlah "aktual".

Apakah ada cara untuk mendapatkan versi kueri parameter untuk bertindak seperti versi literal? Saya ingin menempatkan CTE dalam tampilan yang dapat digunakan kembali.

Pertanyaan dengan literal:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Kueri dengan parameter:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Kode pengaturan:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;
binki
sumber

Jawaban:

12

Randi Vertongen ini jawaban benar alamat bagaimana Anda bisa mendapatkan rencana yang Anda inginkan dengan versi parameter query. Jawaban ini melengkapi bahwa dengan menyinggung judul pertanyaan seandainya Anda tertarik dengan perinciannya.

SQL Server menulis ulang ekspresi tabel bersama (CTE) ekor-rekursif sebagai iterasi. Semuanya dari Spool Down Indeks Malas adalah implementasi runtime dari terjemahan iteratif. Saya menulis akun terperinci tentang bagaimana bagian dari rencana eksekusi ini bekerja sebagai jawaban untuk Menggunakan KECUALI dalam ekspresi tabel umum rekursif .

Anda ingin menentukan predikat (filter) di luar CTE dan meminta pengoptimal kueri mendorong filter ini ke bawah di dalam rekursi (ditulis ulang sebagai iterasi) dan menerapkannya pada anggota anchor. Ini berarti rekursi dimulai dengan hanya catatan yang cocok ParentId = @Id.

Ini harapan yang cukup masuk akal, baik nilai literal, variabel, atau parameter yang digunakan; namun, pengoptimal hanya dapat melakukan hal-hal yang aturannya telah ditulis. Aturan menentukan bagaimana pohon kueri logis diubah untuk mencapai transformasi tertentu. Mereka menyertakan logika untuk memastikan hasil akhirnya aman - yaitu mengembalikan data yang sama persis dengan spesifikasi permintaan asli dalam semua kasus yang mungkin.

Aturan yang bertanggung jawab untuk mendorong predikat pada CTE rekursif disebut SelOnIterator- seleksi relasional (= predikat) pada iterator yang mengimplementasikan rekursi. Lebih tepatnya, aturan ini dapat menyalin pilihan ke bagian jangkar iterasi rekursif:

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Aturan ini dapat dinonaktifkan dengan petunjuk yang tidak berdokumen OPTION(QUERYRULEOFF SelOnIterator). Ketika ini digunakan, pengoptimal tidak dapat lagi mendorong predikat dengan nilai literal ke jangkar CTE rekursif. Anda tidak menginginkan itu, tetapi itu menggambarkan intinya.

Awalnya, aturan ini terbatas untuk mengerjakan predikat dengan nilai literal saja. Itu juga dapat dibuat untuk bekerja dengan variabel atau parameter dengan menentukan OPTION (RECOMPILE), karena petunjuk itu memungkinkan Optimalisasi Penempelan Parameter , di mana nilai literal runtime dari variabel (atau parameter) digunakan ketika menyusun rencana. Rencananya tidak di-cache, jadi downside dari ini adalah kompilasi baru pada setiap eksekusi.

Pada beberapa titik, SelOnIteratoraturan ditingkatkan untuk juga berfungsi dengan variabel dan parameter. Untuk menghindari perubahan rencana yang tidak terduga, ini dilindungi di bawah bendera jejak 4199, tingkat kompatibilitas basis data, dan tingkat kompatibilitas perbaikan terbaru optimizer permintaan. Ini adalah pola yang cukup normal untuk peningkatan pengoptimal, yang tidak selalu didokumentasikan. Perbaikan biasanya baik bagi kebanyakan orang, tetapi selalu ada peluang bahwa perubahan apa pun akan menghasilkan regresi bagi seseorang.

Saya ingin menempatkan CTE dalam tampilan yang dapat digunakan kembali

Anda bisa menggunakan fungsi bernilai tabel inline alih-alih tampilan. Berikan nilai yang ingin Anda tekan sebagai parameter, dan tempatkan predikat tersebut di anggota anchor rekursif.

Jika Anda suka, mengaktifkan jejak bendera 4199 secara global juga merupakan pilihan. Ada banyak perubahan pengoptimal yang dicakup oleh bendera ini, jadi Anda perlu menguji dengan hati-hati beban kerja Anda dengan mengaktifkannya, dan bersiaplah untuk menangani regresi.

Paul White 9
sumber
10

Meskipun saat ini saya tidak memiliki judul perbaikan terbaru yang sebenarnya, rencana kueri yang lebih baik akan digunakan ketika mengaktifkan perbaikan terbaru optimizer kueri pada versi Anda (SQL Server 2012).

Beberapa metode lain adalah:

  • Dengan OPTION(RECOMPILE)begitu penyaringan terjadi lebih awal, pada nilai literal.
  • Pada SQL Server 2016 atau lebih tinggi perbaikan terbaru sebelum versi ini diterapkan secara otomatis dan kueri juga harus berjalan setara dengan paket eksekusi yang lebih baik.

Perbaikan terbaru kueri pengoptimal

Anda dapat mengaktifkan perbaikan ini dengan

  • Traceflag 4199 sebelum SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; mulai dari SQL Server 2016. (tidak diperlukan untuk perbaikan Anda)

Pemfilteran @idditerapkan lebih awal pada anggota rekursif dan jangkar dalam rencana eksekusi dengan perbaikan terbaru yang diaktifkan.

Jejak jejak dapat ditambahkan di tingkat permintaan:

OPTION(QUERYTRACEON 4199)

Saat menjalankan kueri di SQL Server 2012 SP4 GDR atau SQL Server 2014 SP3 dengan Traceflag 4199, rencana kueri yang lebih baik dipilih:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

Paket kueri di SQL Server 2014 SP3 dengan traceflag 4199

Paket kueri pada SQL Server 2012 SP4 GDR dengan traceflag 4199

Paket kueri pada SQL Server 2012 SP4 GDR tanpa traceflag 4199

Konsensus utama adalah untuk mengaktifkan traceflag 4199 secara global ketika menggunakan versi sebelum SQL Server 2016. Setelah itu terbuka untuk diskusi apakah akan mengaktifkannya atau tidak. AQ / A di sini .


Tingkat kompatibilitas 130 atau 140

Saat menguji kueri parameterisasi pada database dengan compatibility_level= 130 atau 140, pemfilteran terjadi lebih awal:

masukkan deskripsi gambar di sini

Karena fakta bahwa perbaikan 'lama' dari traceflag 4199 diaktifkan pada SQL Server 2016 dan lebih tinggi.


PILIHAN (RECOMPILE)

Meskipun prosedur digunakan, SQL Server akan dapat memfilter nilai literal saat menambahkan OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

masukkan deskripsi gambar di sini

Paket kueri pada SQL Server 2012 SP4 GDR dengan OPTION (RECOMPILE)

Randi Vertongen
sumber