Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Indeks yang Difilter dan Parameterisasi Paksa (redux)

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 1
Pemroses 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Huawei GaussDB

  2. Panduan Utama Anda untuk Bergabung dengan SQL:CROSS JOIN – Bagian 3

  3. Kejutan dan Asumsi Kinerja :DATEDIFF

  4. Kasus penggunaan untuk sp_prepare / sp_prepexec

  5. SQL, cara menghapus data dan tabel