V roce 2013 jsem psal o chybě v optimalizátoru, kde 2. a 3. argument pro DATEDIFF()
lze zaměnit – což může vést k nesprávným odhadům počtu řádků a následně ke špatnému výběru plánu realizace:
- Překvapení a předpoklady výkonu:DATEDIFF
Minulý víkend jsem se dozvěděl o podobné situaci a okamžitě jsem předpokládal, že se jedná o stejný problém. Koneckonců, příznaky se zdály téměř stejné:
- V
WHERE
byla funkce data/času doložka.- Tentokrát to bylo
DATEADD()
místoDATEDIFF()
.
- Tentokrát to bylo
- Odhad počtu řádků byl zjevně nesprávný na 1 ve srovnání se skutečným počtem řádků přesahujícím 3 miliony.
- Toto byl ve skutečnosti odhad 0, ale SQL Server tyto odhady vždy zaokrouhluje na 1.
- Byl proveden špatný výběr plánu (v tomto případě bylo zvoleno spojení smyčky) kvůli nízkému odhadu.
Urážlivý vzor vypadal takto:
WHERE [datetime2(7) column] >= DATEADD(DAY, -365, SYSUTCDATETIME());
Uživatel vyzkoušel několik variant, ale nic se nezměnilo; nakonec se jim podařilo problém obejít změnou predikátu na:
WHERE DATEDIFF(DAY, [column], SYSUTCDATETIME()) <= 365;
Toto získalo lepší odhad (typický odhad 30% nerovnosti); takže ne úplně správně. A i když to eliminovalo spojení smyčky, tento predikát má dva hlavní problémy:
- To není stejný dotaz, protože nyní hledá hranice 365 dnů, které uplynuly, na rozdíl od toho, aby byly větší než konkrétní časový bod před 365 dny. Statisticky významný? Možná ne. Technicky ale parapet není stejný.
- Použití funkce proti sloupci způsobí, že se celý výraz nebude měnit, což vede k úplnému skenování. Když tabulka obsahuje data jen něco málo přes rok, není to velký problém, ale jak se tabulka zvětšuje nebo se predikát zužuje, bude to problém.
Znovu jsem skočil k závěru, že DATEADD()
operace byla problémem a doporučila přístup, který se nespoléhal na DATEADD()
– vytvoření datetime
ze všech částí aktuálního času, což mi umožňuje odečíst rok bez použití DATEADD()
:
WHERE [column] >= DATETIMEFROMPARTS( DATEPART(YEAR, SYSUTCDATETIME())-1, DATEPART(MONTH, SYSUTCDATETIME()), DATEPART(DAY, SYSUTCDATETIME()), DATEPART(HOUR, SYSUTCDATETIME()), DATEPART(MINUTE, SYSUTCDATETIME()), DATEPART(SECOND, SYSUTCDATETIME()), 0);
Kromě toho, že to bylo objemné, mělo to své vlastní problémy, konkrétně to, že bylo třeba přidat spoustu logiky, aby se správně zohlednily přestupné roky. Zaprvé, aby neselhal, kdyby náhodou poběží 29. února, a zadruhé, aby ve všech případech zahrnoval přesně 365 dní (místo 366 během roku následujícího po přestupném dni). Snadné opravy, samozřejmě, ale dělají logiku mnohem ošklivější – zejména proto, že dotaz musel existovat uvnitř pohledu, kde meziproměnné a vícenásobné kroky nejsou možné.
Mezitím OP zadal položku Connect, zděšen odhadem o 1 řádku:
- Connect #2567628 :Omezení s DateAdd() neposkytuje dobré odhady
Pak přišel Paul White (@SQL_Kiwi) a jako mnohokrát předtím vnesl do problému další světlo. Sdílel související položku Connect, kterou podal Erland Sommarskog v roce 2011:
- Connect #685903 :Nesprávný odhad, když se sysdatetime objeví ve výrazu dateadd()
Problém je v podstatě v tom, že špatný odhad lze udělat nejen když SYSDATETIME()
(nebo SYSUTCDATETIME()
) se objeví, jak Erland původně uvedl, ale při jakémkoli datetime2
výraz je zahrnut v predikátu (a možná pouze tehdy, když DATEADD()
se také používá). A může to jít oběma způsoby – pokud prohodíme >=
pro <=
, odhad se stane celou tabulkou, takže se zdá, že se optimalizátor dívá na SYSDATETIME()
hodnotu jako konstantu a zcela ignoruje všechny operace jako DATEADD()
které se proti němu provádějí.
Paul sdílel, že řešením je jednoduše použít datetime
ekvivalentní při výpočtu data, než jej převedete na správný datový typ. V tomto případě můžeme vyměnit SYSUTCDATETIME()
a změňte jej na GETUTCDATE()
:
WHERE [column] >= CONVERT(datetime2(7), DATEADD(DAY, -365, GETUTCDATE()));
Ano, to má za následek nepatrnou ztrátu přesnosti, ale stejně tak může částečka prachu zpomalit váš prst na cestě ke stisknutí
Čtení jsou podobná, protože tabulka obsahuje data téměř výhradně z minulého roku, takže i hledání se stává rozsahovým skenem většiny tabulky. Počty řádků nejsou identické, protože (a) druhý dotaz končí o půlnoci a (b) třetí dotaz obsahuje další den dat kvůli přestupnému dni dříve v tomto roce. V každém případě to stále ukazuje, jak se můžeme přiblížit správným odhadům odstraněním DATEADD()
, ale správnou opravou je odstranit přímou kombinaci z DATEADD()
a datetime2
.
Abychom dále ilustrovali, jak se odhady pletou, můžete vidět, že pokud původnímu dotazu a Pavlovu přepsání předáme různé argumenty a pokyny, počet odhadovaných řádků pro první je vždy založen na aktuálním čase – ne 't změnit s počtem uplynulých dnů (zatímco Paul's je relativně přesné pokaždé):
Skutečné řádky pro první dotaz jsou o něco nižší, protože byl proveden po dlouhém spánku
Odhady nebudou vždy tak dobré; moje tabulka má relativně stabilní distribuci. Vyplnil jsem jej následujícím dotazem a poté aktualizoval statistiky pomocí fullscan pro případ, že byste to chtěli vyzkoušet na vlastní kůži:
-- OP's table definition: CREATE TABLE dbo.DateaddRepro ( SessionId int IDENTITY(1, 1) NOT NULL PRIMARY KEY, CreatedUtc datetime2(7) NOT NULL DEFAULT SYSUTCDATETIME() ); GO CREATE NONCLUSTERED INDEX [IX_User_Session_CreatedUtc] ON dbo.DateaddRepro(CreatedUtc) INCLUDE (SessionId); GO INSERT dbo.DateaddRepro(CreatedUtc) SELECT dt FROM ( SELECT TOP (3150000) dt = DATEADD(HOUR, (s1.[precision]-ROW_NUMBER() OVER (PARTITION BY s1.[object_id] ORDER BY s2.[object_id])) / 15, GETUTCDATE()) FROM sys.all_columns AS s1 CROSS JOIN sys.all_objects AS s2 ) AS x; UPDATE STATISTICS dbo.DateaddRepro WITH FULLSCAN; SELECT DISTINCT SessionId FROM dbo.DateaddRepro WHERE /* pick your WHERE clause to test */;
Přidal jsem komentář k nové položce Connect a pravděpodobně se vrátím a upravím svou odpověď Stack Exchange.
Morálka příběhu
Snažte se vyhnout kombinaci DATEADD()
s výrazy, které dávají datetime2
, zejména na starších verzích SQL Server (to bylo na SQL Server 2012). Může to být také problém, dokonce i na SQL Server 2016, při použití staršího modelu odhadu mohutnosti (kvůli nižší úrovni kompatibility nebo explicitnímu použití příznaku trasování 9481). Problémy jako tento jsou nenápadné a ne vždy jsou okamžitě zřejmé, takže doufám, že to poslouží jako připomenutí (možná i pro mě, až příště narazím na podobný scénář). Jak jsem navrhl v minulém příspěvku, pokud máte takové vzorce dotazů, zkontrolujte, zda získáváte správné odhady, a udělejte si někde poznámku, abyste je mohli znovu zkontrolovat, kdykoli se v systému něco zásadního změní (jako je upgrade nebo aktualizace Service Pack).