sql >> Databáze >  >> RDS >> Sqlserver

Proč SQL Server používá index scan místo index search, když klauzule WHERE obsahuje parametrizované hodnoty

Abychom odpověděli na vaši otázku, proč to SQL Server dělá, odpověď je, že dotaz není kompilován v logickém pořadí, každý příkaz je kompilován na základě své vlastní hodnoty, takže když se generuje plán dotazů pro váš příkaz select, optimalizátor neví, že @val1 a @Val2 se stanou 'val1' a 'val2' v tomto pořadí.

Když SQL Server nezná hodnotu, musí co nejlépe odhadnout, kolikrát se tato proměnná objeví v tabulce, což může někdy vést k suboptimálním plánům. Můj hlavní bod je, že stejný dotaz s různými hodnotami může generovat různé plány. Představte si tento jednoduchý příklad:

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP 991 1
FROM    sys.all_objects a
UNION ALL
SELECT  TOP 9 ROW_NUMBER() OVER(ORDER BY a.object_id) + 1
FROM    sys.all_objects a;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

Vše, co jsem zde udělal, je vytvořit jednoduchou tabulku a přidat 1000 řádků s hodnotami 1-10 pro sloupec val 1 se však objeví 991krát a zbývajících 9 se objeví pouze jednou. Předpokladem je tento dotaz:

SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = 1;

Efektivnější by bylo pouze prohledat celou tabulku, než použít index pro hledání a poté provést 991 vyhledávání v záložkách, abyste získali hodnotu Filler , avšak pouze s 1 řádkem následující dotaz:

SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = 2;

bude efektivnější provádět vyhledávání indexu a vyhledávání jedné záložky pro získání hodnoty Filler (a spuštění těchto dvou dotazů to potvrdí)

Jsem si docela jistý, že hranice pro vyhledávání a vyhledávání záložek se skutečně liší v závislosti na situaci, ale je poměrně nízká. Pomocí ukázkové tabulky jsem s trochou pokusů a omylů zjistil, že potřebuji Val aby měl sloupec 38 řádků s hodnotou 2, než optimalizátor přešel na úplné prohledání tabulky přes hledání indexu a vyhledávání záložek:

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

DECLARE @I INT = 38;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP (991 - @i) 1
FROM    sys.all_objects a
UNION ALL
SELECT  TOP (@i) 2
FROM    sys.all_objects a
UNION ALL
SELECT  TOP 8 ROW_NUMBER() OVER(ORDER BY a.object_id) + 2
FROM    sys.all_objects a;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

SELECT  COUNT(Filler), COUNT(*)
FROM    #T
WHERE   Val = 2;

Takže pro tento příklad je limit 3,7 % odpovídajících řádků.

Protože dotaz nezná, kolik řádků se bude shodovat, když používáte proměnnou, musí uhodnout, a nejjednodušší způsob je zjistit celkový počet řádků a vydělit ho celkovým počtem odlišných hodnot ve sloupci, takže v tomto příkladu odhadovaný počet řádků pro WHERE val = @Val je 1000 / 10 =100, Skutečný algoritmus je složitější než tento, ale například to bude stačit. Když se tedy podíváme na plán provádění:

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

Zde (s původními daty) můžeme vidět, že odhadovaný počet řádků je 100, ale skutečný počet řádků je 1. Z předchozích kroků víme, že s více než 38 řádky se optimalizátor rozhodne pro skenování sdruženého indexu přes index hledat, takže protože nejlepší odhad počtu řádků je vyšší než tento, plán pro neznámou proměnnou je skenování shlukovaného indexu.

Jen pro další doložení teorie, pokud vytvoříme tabulku s 1000 řádky čísel 1-27 rovnoměrně rozložených (takže odhadovaný počet řádků bude přibližně 1000 / 27 =37,037)

IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
    DROP TABLE #T;

CREATE TABLE #T (ID INT IDENTITY PRIMARY KEY, Val INT NOT NULL, Filler CHAR(1000) NULL);
INSERT #T (Val)
SELECT  TOP 27 ROW_NUMBER() OVER(ORDER BY a.object_id)
FROM    sys.all_objects a;

INSERT #T (val)
SELECT  TOP 973 t1.Val
FROM    #T AS t1
        CROSS JOIN #T AS t2
        CROSS JOIN #T AS t3
ORDER BY t2.Val, t3.Val;

CREATE NONCLUSTERED INDEX IX_T__Val ON #T (Val);

Poté spusťte dotaz znovu, získáme plán s hledáním indexu:

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

Doufejme, že to docela obsáhle pokryje, proč jste získali tento plán. Nyní předpokládám, že další otázkou je, jak vynutit jiný plán, a odpovědí je použít nápovědu k dotazu OPTION (RECOMPILE) , k vynucení kompilace dotazu v době provádění, kdy je známá hodnota parametru. Návrat k původním datům, kde je nejlepší plán pro Val = 2 je vyhledávání, ale pomocí proměnné získáme plán s indexovým skenováním, můžeme spustit:

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i;

GO

DECLARE @i INT = 2;
SELECT  COUNT(Filler)
FROM    #T
WHERE   Val = @i
OPTION (RECOMPILE);

Vidíme, že druhý používá vyhledávání indexu a vyhledávání klíčů, protože zkontroloval hodnotu proměnné v době provádění a pro tuto konkrétní hodnotu je vybrán nejvhodnější plán. Problém s OPTION (RECOMPILE) to znamená, že nemůžete využít plánů dotazů uložených v mezipaměti, takže pokaždé vznikají dodatečné náklady na kompilaci dotazu.



  1. Jak importovat soubor .bak SQL Serveru do MySQL?

  2. Limity kurzoru dotazu Salesforce API

  3. SQL Vnitřní spojení na vybraných příkazech

  4. Prozkoumání možností čekání na zámek s nízkou prioritou v SQL Server 2014 CTP1