Setelah membuat blog tentang bagaimana indeks yang difilter dapat menjadi lebih kuat, dan baru-baru ini tentang bagaimana indeks tersebut dapat dianggap tidak berguna dengan parameterisasi paksa, saya meninjau kembali topik indeks/parameterisasi yang difilter. Solusi yang tampaknya terlalu sederhana muncul di tempat kerja baru-baru ini, dan saya harus membagikannya.
Ambil contoh berikut, di mana kami memiliki database penjualan yang berisi tabel pesanan. Terkadang kami hanya ingin daftar (atau hitungan) hanya pesanan yang belum dikirim — yang, seiring waktu, (semoga!) mewakili persentase yang lebih kecil dan lebih kecil dari keseluruhan tabel:
CREATE DATABASE Sales; GO USE Sales; GO -- simplified, obviously: CREATE TABLE dbo.Orders ( OrderID int IDENTITY(1,1) PRIMARY KEY, OrderDate datetime NOT NULL, filler char(500) NOT NULL DEFAULT '', IsShipped bit NOT NULL DEFAULT 0 ); GO -- let's put some data in there; 7,000 shipped orders, and 50 unshipped: INSERT dbo.Orders(OrderDate, IsShipped) -- random dates over two years SELECT TOP (7000) DATEADD(DAY, ABS(object_id % 730), '20171101'), 1 FROM sys.all_columns UNION ALL -- random dates from this month SELECT TOP (50) DATEADD(DAY, ABS(object_id % 30), '20191201'), 0 FROM sys.all_columns;
Mungkin masuk akal dalam skenario ini untuk membuat indeks yang difilter seperti ini (yang membuat pekerjaan cepat dari setiap kueri yang mencoba untuk mendapatkan pesanan yang belum terkirim itu):
CREATE INDEX ix_OrdersNotShipped ON dbo.Orders(IsShipped, OrderDate) WHERE IsShipped = 0;
Kita dapat menjalankan kueri cepat seperti ini untuk melihat cara menggunakan indeks yang difilter:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
Rencana eksekusi cukup sederhana, tetapi ada peringatan tentang UnmatchedIndexes:
Nama peringatannya sedikit menyesatkan — pengoptimal pada akhirnya dapat menggunakan indeks, tetapi menyarankan agar "lebih baik" tanpa parameter (yang tidak kami gunakan secara eksplisit), meskipun pernyataan tersebut tampaknya memiliki parameter:
Jika Anda benar-benar ingin, Anda dapat menghilangkan peringatan, tanpa perbedaan dalam kinerja sebenarnya (itu hanya kosmetik). Salah satu caranya adalah dengan menambahkan predikat zero-impact, seperti AND (1 > 0)
:
SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
Cara lain (mungkin lebih umum) adalah menambahkan OPTION (RECOMPILE)
:
SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
Kedua opsi ini menghasilkan rencana yang sama (pencarian tanpa peringatan):
Sejauh ini baik; indeks terfilter kami sedang digunakan (seperti yang diharapkan). Ini bukan satu-satunya trik, tentu saja; lihat komentar di bawah untuk orang lain yang telah dikirimkan oleh pembaca.
Lalu, komplikasinya
Karena database tunduk pada sejumlah besar kueri ad hoc, seseorang mengaktifkan parameterisasi paksa, mencoba untuk mengurangi kompilasi dan menghilangkan rencana penggunaan rendah dan sekali pakai agar tidak mencemari cache rencana:
ALTER DATABASE Sales SET PARAMETERIZATION FORCED;
Sekarang kueri asli kami tidak dapat menggunakan indeks yang difilter; itu dipaksa untuk memindai indeks berkerumun:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
Peringatan tentang indeks yang tidak cocok kembali, dan kami mendapatkan peringatan baru tentang sisa I/O. Perhatikan bahwa pernyataan tersebut berparameter, tetapi terlihat sedikit berbeda:
Ini dirancang, karena seluruh tujuan parameterisasi paksa adalah untuk membuat parameter kueri seperti ini. Tapi itu mengalahkan tujuan indeks terfilter kami, karena itu dimaksudkan untuk mendukung satu nilai dalam predikat, bukan parameter yang dapat berubah.
Kebodohan
Kueri "trik" kami yang menggunakan predikat tambahan juga tidak dapat menggunakan indeks yang difilter, dan berakhir dengan rencana boot yang sedikit lebih rumit:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
OPSI (KOMPILASI ULANG)
Reaksi khas dalam kasus ini, sama seperti menghapus peringatan sebelumnya, adalah menambahkan OPTION (RECOMPILE)
ke pernyataan. Ini berfungsi, dan memungkinkan indeks yang difilter dipilih untuk pencarian yang efisien…
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
…tetapi menambahkan OPTION (RECOMPILE)
dan mengambil kompilasi tambahan ini untuk setiap eksekusi kueri tidak selalu dapat diterima di lingkungan volume tinggi (terutama jika mereka sudah terikat dengan CPU).
Petunjuk
Seseorang menyarankan secara eksplisit mengisyaratkan indeks yang difilter untuk menghindari biaya kompilasi ulang. Secara umum, ini agak rapuh, karena bergantung pada indeks yang hidup lebih lama dari kode; Saya cenderung menggunakan ini sebagai upaya terakhir. Dalam hal ini tetap tidak valid. Ketika aturan parameterisasi mencegah pengoptimal memilih indeks yang difilter secara otomatis, aturan tersebut juga mencegah Anda mengambilnya secara manual. Masalah yang sama dengan FORCESEEK
umum petunjuk:
SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0; SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;
Keduanya menghasilkan kesalahan ini:
Pesan 8622, Level 16, Status 1Pemroses kueri tidak dapat menghasilkan rencana kueri karena petunjuk yang ditentukan dalam kueri ini. Kirim ulang kueri tanpa menentukan petunjuk apa pun dan tanpa menggunakan SET FORCEPLAN.
Dan ini masuk akal, karena tidak ada cara untuk mengetahui bahwa nilai yang tidak diketahui untuk IsShipped
parameter akan cocok dengan indeks yang difilter (atau mendukung operasi pencarian pada indeks apa pun).
SQL Dinamis?
Saya menyarankan Anda dapat menggunakan SQL dinamis, setidaknya hanya membayar hit kompilasi ulang ketika Anda tahu Anda ingin mencapai indeks yang lebih kecil:
DECLARE @IsShipped bit = 0; DECLARE @sql nvarchar(max) = N'SELECT dynsql = OrderID, OrderDate FROM dbo.Orders' + CASE WHEN @IsShipped IS NOT NULL THEN N' WHERE IsShipped = @IsShipped' ELSE N'' END + CASE WHEN @IsShipped = 0 THEN N' OPTION (RECOMPILE)' ELSE N'' END; EXEC sys.sp_executesql @sql, N'@IsShipped bit', @IsShipped;
Ini mengarah pada rencana efisien yang sama seperti di atas. Jika Anda mengubah variabel menjadi @IsShipped = 1
, maka Anda mendapatkan pemindaian indeks berkerumun yang lebih mahal yang Anda harapkan:
Tapi tidak ada yang suka menggunakan SQL dinamis dalam kasus tepi seperti ini — itu membuat kode lebih sulit untuk dibaca dan dipelihara, dan bahkan jika kode ini keluar dalam aplikasi, itu masih logika tambahan yang harus ditambahkan di sana, membuatnya kurang diinginkan .
Sesuatu yang lebih sederhana
Kami berbicara secara singkat tentang menerapkan panduan rencana, yang tentu saja tidak sederhana, tetapi kemudian seorang rekan menyarankan agar Anda dapat menipu pengoptimal dengan "menyembunyikan" pernyataan berparameter di dalam prosedur tersimpan, tampilan, atau fungsi bernilai tabel sebaris. Itu sangat sederhana, saya tidak percaya itu akan berhasil.
Tapi kemudian saya mencobanya:
CREATE PROCEDURE dbo.GetUnshippedOrders AS BEGIN SET NOCOUNT ON; SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; END GO CREATE VIEW dbo.vUnshippedOrders AS SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; GO CREATE FUNCTION dbo.fnUnshippedOrders() RETURNS TABLE AS RETURN (SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0); GO
Ketiga kueri ini melakukan pencarian yang efisien terhadap indeks yang difilter:
EXEC dbo.GetUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();
Kesimpulan
Saya terkejut ini sangat efektif. Tentu saja, ini mengharuskan Anda untuk mengubah aplikasi; jika Anda tidak dapat mengubah kode aplikasi untuk memanggil prosedur tersimpan atau mereferensikan tampilan atau fungsi (atau bahkan menambahkan OPTION (RECOMPILE)
), Anda harus terus mencari opsi lain. Tetapi jika Anda dapat mengubah kode aplikasi, memasukkan predikat ke modul lain mungkin merupakan cara yang tepat.