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