sql >> Databáze >  >> RDS >> Database

Filtrované indexy a vynucená parametrizace (redux)

Poté, co jsem blogoval o tom, jak by filtrované indexy mohly být výkonnější, a nedávno o tom, jak je lze pomocí nucené parametrizace znehodnotit, znovu se vracím k tématu filtrované indexy/parametrizace. Nedávno se v práci objevilo zdánlivě příliš jednoduché řešení a musel jsem se o něj podělit.

Vezměte si následující příklad, kde máme prodejní databázi obsahující tabulku objednávek. Někdy potřebujeme pouze seznam (nebo počet) pouze objednávek, které ještě nebyly odeslány – které časem (doufejme!) představují menší a menší procento celkové tabulky:

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;

V tomto scénáři může mít smysl vytvořit filtrovaný index, jako je tento (což umožňuje rychlou práci se všemi dotazy, které se snaží získat tyto neodeslané objednávky):

CREATE INDEX ix_OrdersNotShipped 
  ON dbo.Orders(IsShipped, OrderDate) 
  WHERE IsShipped = 0;

Můžeme spustit rychlý dotaz, jako je tento, abychom viděli, jak používá filtrovaný index:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

Plán provádění je poměrně jednoduchý, ale existuje varování o UnmatchedIndexes:

Název varování je mírně zavádějící – optimalizátor nakonec dokázal index použít, ale naznačuje, že by to bylo „lepší“ bez parametrů (které jsme explicitně nepoužili), i když příkaz vypadá, jako by byl parametrizován:

Pokud opravdu chcete, můžete varování odstranit, bez rozdílu ve skutečném výkonu (bylo by to jen kosmetické). Jedním ze způsobů je přidat predikát s nulovým dopadem, například AND (1 > 0) :

SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

Další (pravděpodobně běžnější) je přidat OPTION (RECOMPILE) :

SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

Obě tyto možnosti poskytují stejný plán (vyhledávání bez varování):

Zatím je vše dobré; náš filtrovaný index se používá (podle očekávání). To samozřejmě nejsou jediné triky; podívejte se na komentáře níže pro ostatní, které čtenáři již odeslali.

Pak komplikace

Protože databáze podléhá velkému počtu ad hoc dotazů, někdo zapne vynucenou parametrizaci ve snaze omezit kompilaci a eliminovat málo a jednorázové plány, aby znečišťovaly mezipaměť plánu:

ALTER DATABASE Sales SET PARAMETERIZATION FORCED;

Náš původní dotaz nyní nemůže použít filtrovaný index; je nucen prohledat seskupený index:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

Vrátí se varování o neshodných indexech a dostáváme nová varování o zbytkových I/O. Všimněte si, že příkaz je parametrizovaný, ale vypadá trochu jinak:

Toto je záměrné, protože celým účelem vynucené parametrizace je parametrizovat dotazy, jako je tento. Ale maří to účel našeho filtrovaného indexu, protože ten má podporovat jedinou hodnotu v predikátu, nikoli parametr, který se může změnit.

Tomfoolery

Náš „trikový“ dotaz, který používá další predikát, také nedokáže použít filtrovaný index a končí s trochu komplikovanějším plánem zavedení:

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

OPTION (REKOMPILOVAT)

Typickou reakcí v tomto případě, stejně jako v případě předchozího odstranění varování, je přidání OPTION (RECOMPILE) k prohlášení. To funguje a umožňuje výběr filtrovaného indexu pro efektivní vyhledávání…

SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

…ale přidáním OPTION (RECOMPILE) a použití tohoto dodatečného kompilačního zásahu proti každému provedení dotazu nebude vždy přijatelné v prostředích s velkým objemem (zejména pokud jsou již vázána na CPU).

Rady

Někdo navrhl explicitně naznačit filtrovaný index, aby se zabránilo nákladům na rekompilaci. Obecně je to poněkud křehké, protože se spoléhá na index, který přežije kód; Mám tendenci to používat jako poslední možnost. V tomto případě to stejně není platné. Když pravidla parametrizace zabraňují optimalizátoru v automatickém výběru filtrovaného indexu, brání vám také v ručním výběru. Stejný problém s obecným FORCESEEK nápověda:

SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0;
 
SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;

Oba způsobí tuto chybu:

Msg 8622, Level 16, State 1
Procesor dotazu nemohl vytvořit plán dotazů kvůli radám definovaným v tomto dotazu. Znovu odešlete dotaz bez zadání jakýchkoli rad a bez použití SET FORCEPLAN.

A to dává smysl, protože neexistuje způsob, jak zjistit, že neznámá hodnota pro IsShipped Parametr bude odpovídat filtrovanému indexu (nebo bude podporovat operaci vyhledávání na jakémkoli indexu).

Dynamické SQL?

Navrhl jsem, že byste mohli použít dynamický SQL, abyste zaplatili tento rekompilační přístup pouze tehdy, když víte, že chcete zasáhnout menší index:

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;

To vede ke stejně efektivnímu plánu jako výše. Pokud jste změnili proměnnou na @IsShipped = 1 , pak získáte dražší skenování clusteru indexu, které byste měli očekávat:

Ale nikdo nemá rád používání dynamického SQL v okrajovém případě, jako je tento – kód se tím hůř čte a udržuje, a i kdyby tento kód byl v aplikaci mimo, stále je to další logika, která by tam musela být přidána, takže je méně než žádoucí. .

Něco jednoduššího

Krátce jsme mluvili o implementaci průvodce plánem, který rozhodně není jednodušší, ale pak kolega navrhl, že byste mohli optimalizátor oklamat „skrytím“ parametrizovaného příkazu uvnitř uložené procedury, pohledu nebo funkce s hodnotou vložené tabulky. Bylo to tak jednoduché, že jsem nevěřil, že to bude fungovat.

Ale pak jsem to zkusil:

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

Všechny tři tyto dotazy provádějí efektivní vyhledávání proti filtrovanému indexu:

EXEC dbo.GetUnshippedOrders;
GO
SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders;
GO
SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();

Závěr

Překvapilo mě, že to bylo tak účinné. To samozřejmě vyžaduje změnu aplikace; pokud nemůžete změnit kód aplikace tak, aby volal uloženou proceduru nebo odkazoval na zobrazení nebo funkci (nebo dokonce přidat OPTION (RECOMPILE) ), budete muset hledat další možnosti. Ale pokud můžete změnit kód aplikace, nacpat predikát do jiného modulu může být cesta.


  1. Postgres:definovat výchozí hodnotu pro selhání CAST?

  2. Chyba syntaxe na konci vstupu v PostgreSQL

  3. Vyberte sloupce s konkrétními názvy sloupců v PostgreSQL

  4. Jak správně vložit nový řádek do nvarchar