Nejběžnější potřeba pro odstranění času z hodnoty datetime je získat všechny řádky, které představují objednávky (nebo návštěvy nebo nehody), ke kterým došlo v daný den. Ne všechny techniky, které se k tomu používají, jsou však účinné nebo dokonce bezpečné.
Verze TL;DR
Pokud chcete dotaz na bezpečný rozsah, který funguje dobře, použijte rozsah s otevřeným koncem nebo pro jednodenní dotazy na serveru SQL Server 2008 a vyšším použijte CONVERT(DATE)
:
DECLARE @today DATETIME; -- pouze v <=2005:SET @dnes =DATEADD(DEN, DATEDIFF(DEN, '20000101', CURRENT_TIMESTAMP), '20000101'); -- nebo v roce 2008 a výše:SET @dnes =CONVERT(DATE, CURRENT_TIMESTAMP); -- a poté v dotazu použijte rozsah s otevřeným koncem:...WHERE Datum objednávky>=@dnes AND Datum objednávkyNěkterá upozornění:
- Dejte si pozor na
DATEDIFF
přístup, protože mohou nastat určité anomálie odhadu mohutnosti (další informace naleznete v tomto příspěvku na blogu a v otázce Přetečení zásobníku, která jej podnítila).- Přestože poslední z nich bude stále potenciálně používat vyhledávání indexu (na rozdíl od všech ostatních výrazů bez přeměny, se kterými jsem se kdy setkal), musíte být opatrní při převodu sloupce na datum před porovnáváním. I tento přístup může vést k zásadně nesprávným odhadům mohutnosti. Další podrobnosti naleznete v této odpovědi Martina Smithe.
V každém případě čtěte dále, abyste pochopili, proč jsou to jediné dva přístupy, které kdy doporučuji.
Ne všechny přístupy jsou bezpečné
Jako nebezpečný příklad vidím tento často používaný:
WHERE OrderDate BETWEEN DATEDIFF(DAY, 0, GETDATE()) AND DATEADD(MILISECOND, -3, DATEDIFF(DAY, 0, GETDATE()) + 1);Tento přístup má několik problémů, ale tím nejpozoruhodnějším je výpočet „konce“ dneška – pokud je základní datový typ
SMALLDATETIME
, tento koncový rozsah se zaokrouhlí nahoru; pokud jeDATETIME2
, teoreticky by vám mohla chybět data na konci dne. Pokud zvolíte minuty nebo nanosekundy nebo jakoukoli jinou mezeru pro přizpůsobení aktuálnímu datovému typu, váš dotaz se začne chovat divně, pokud se datový typ později změní (a buďme upřímní, pokud někdo změní typ sloupce tak, aby byl více či méně granulární, neběhají a kontrolují každý jednotlivý dotaz, který k němu přistupuje). Nutnost kódovat tímto způsobem v závislosti na typu dat data/času v podkladovém sloupci je fragmentovaná a náchylná k chybám. K tomu je mnohem lepší použít časově neomezená období:Mluvím o tom mnohem více v několika starých příspěvcích na blogu:
- Co mají společného BETWEEN a ďábel?
- Špatné návyky:nesprávné zpracování dotazů na datum/rozsah
Ale chtěl jsem porovnat výkon některých běžnějších přístupů, které tam vidím. Vždy jsem používal rozsahy s otevřeným koncem a od SQL Server 2008 jsme mohli používat
CONVERT(DATE)
a stále používat index na tomto sloupci, který je docela výkonný.SELECT CONVERT(CHAR(8), CURRENT_TIMESTAMP, 112);SELECT CONVERT(CHAR(10), CURRENT_TIMESTAMP, 120);SELECT CONVERT(DATE, CURRENT_TIMESTAMP);SELECT DATEADD(DAY, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP), '19000101');SELECT CONVERT(DATETIME, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP));SELECT CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, CURRENT_TIMESTAMP)(DATE)); CONVERT(FLOAT, CURRENT_TIMESTAMP)));Jednoduchý test výkonu
Abych provedl velmi jednoduchý počáteční test výkonu, provedl jsem pro každý z výše uvedených příkazů následující a nastavil proměnnou na výstup výpočtu 100 000krát:
SELECT SYSDATETIME();GO DECLARE @d DATETIME =[metoda převodu];GO 100000 SELECT SYSDATETIME();GOU každé metody jsem to udělal třikrát a všechny běžely v rozmezí 34-38 sekund. Takže přísně vzato, existují velmi zanedbatelné rozdíly v těchto metodách při provádění operací v paměti:
Propracovanější test výkonu
Také jsem chtěl porovnat tyto metody s různými datovými typy (
DATETIME
,SMALLDATETIME
aDATETIME2
), proti klastrovanému indexu i haldě a s kompresí dat a bez ní. Nejprve jsem tedy vytvořil jednoduchou databázi. Experimentováním jsem zjistil, že optimální velikost pro zpracování 120 milionů řádků a veškeré aktivity protokolu, která by se mohla vyskytnout (a aby se zabránilo rušení událostí automatického růstu při testování), je 20GB datový soubor a 3GB protokol:VYTVOŘTE DATABÁZI [Datetime_Testing]NA PRIMÁRNÍ ( NAME =N'Datetime_Testing_Data', FILENAME =N'D:\DATA\Datetime_Testing.mdf', VELIKOST =20480000 kB, MAXSIZE =NEOMEZENÉ, 20LOG =NEOMEZENÉ, FILEGROB 'Datetime_Testing_Log', FILENAME =N'E:\LOGS\Datetime_Testing_log.ldf', VELIKOST =3000000 kB, MAXSIZE =NEOMEZENO, FILEGROWTH =20480 KB);Dále jsem vytvořil 12 tabulek:
-- clusterovaný index bez komprese:CREATE TABLE dbo.smalldatetime_nocompression_clustered(dt SMALLDATETIME);CREATE CLUSTERED INDEX x ON dbo.smalldatetime_nocompression_clustered(dt); -- halda bez komprese:CREATE TABLE dbo.smalldatetime_nocompression_heap(dt SMALLDATETIME); -- seskupený index s kompresí stránky:CREATE TABLE dbo.smalldatetime_compression_clustered(dt SMALLDATETIME) WITH (DATA_COMPRESSION =PAGE); CREATE CLUSTERED INDEX x ON dbo.smalldatetime_compression_clustered(dt)WITH (DATA_COMPRESSION =PAGE); -- halda s kompresí stránky:CREATE TABLE dbo.smalldatetime_compression_heap(dt SMALLDATETIME)WITH (DATA_COMPRESSION =PAGE);[Pak opakujte znovu pro DATETIME a DATETIME2.]
Dále jsem do každé tabulky vložil 10 000 000 řádků. Udělal jsem to tak, že jsem vytvořil pohled, který by pokaždé vygeneroval stejných 10 000 000 dat:
VYTVOŘIT ZOBRAZENÍ dbo.TenMillionDatesAS SELECT TOP (10000000) d =DATEADD(MINUTE, ŘÁDEK_ČÍSLO() PŘES (ORDER BY s1.[id_object]), '19700101') OD sys.all_columns ASIN s1 CROSS.JO ORDER BY s1.[object_id];To mi umožnilo naplnit tabulky tímto způsobem:
INSERT /* dt_comp_clus */ dbo.datetime_compression_clustered(dt) SELECT CONVERT(DATETIME, d) FROM dbo.TenMillionDates;CHECKPOINT;INSERT /* dt2_comp_clus */ dbo.datetime2_komprese dbo.datetime2_DATE(dtboTIME CON. .TenMillionDates;CHECKPOINT;INSERT /* sdt_comp_clus */ dbo.smalldatetime_compression_clustered(dt) SELECT CONVERT(SMALLDATETIME, d) Z dbo.TenMillionDates;CHECKPOINT;[Pak opakujte znovu pro haldy a nekomprimovaný seskupený index. Vložil jsem
CHECKPOINT
mezi každou vložkou, aby bylo zajištěno opětovné použití protokolu (model obnovy je jednoduchý).]VLOŽTE časování a použitý prostor
Zde jsou načasování pro každou vložku (jak bylo zachyceno pomocí Plan Explorer):
A zde je množství místa, které zabírají jednotlivé tabulky:
SELECT [tabulka] =OBJECT_NAME([id_objektu]), počet_řádků, počet_stránek =počet_rezervovaných_stránek, vyhrazená_velikost_MB =počet_rezervovaných_stránek * 8/1024FROM sys.dm_db_partition_stats WHERE OBJECT_NAME([id_objektu]) LIKE '%predate>%'
Výkon vzoru dotazu
Dále jsem se rozhodl otestovat dva různé vzory dotazů na výkon:
- Počítání řádků pro konkrétní den pomocí výše uvedených sedmi přístupů a také otevřené časové období
- Převod všech 10 000 000 řádků pomocí výše uvedených sedmi přístupů a také pouhé vrácení nezpracovaných dat (protože formátování na straně klienta může být lepší)
[S výjimkou
FLOAT
metody aDATETIME2
protože tato konverze není legální.]U první otázky vypadají dotazy takto (opakované pro každý typ tabulky):
SELECT /* C_CHAR10 - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(CHAR(10), dt, 120) ='19860301'; SELECT /* C_CHAR8 - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(CHAR(8), dt, 112) ='19860301'; SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) ='19860301'; SELECT /* C_DATETIME - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) ='19860301'; SELECT /* C_DATE - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATE, dt) ='19860301'; SELECT /* C_INT_FLOAT - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) ='19860301'; SELECT /* DATEADD - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') ='19860301 SELECT /* ROZSAH - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE dt>='19860301' AND dt <'19860302';Výsledky proti shlukovanému indexu vypadají takto (kliknutím zvětšíte):
Zde vidíme, že převod na datum a otevřený rozsah pomocí indexu jsou nejlepší. V porovnání s hromadou však převod na datum ve skutečnosti nějakou dobu trvá, takže otevřený rozsah je optimální volbou (kliknutím zvětšíte):
A zde je druhá sada dotazů (opět opakujících se pro každý typ tabulky):
SELECT /* C_CHAR10 - dt_comp_clus */ dt =CONVERT(CHAR(10), dt, 120) FROM dbo.datetime_compression_clustered; SELECT /* C_CHAR8 - dt_comp_clus */ dt =CONVERT(CHAR(8), dt, 112) FROM dbo.datetime_compression_clustered; SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ dt =CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) FROM dbo.datetime_compression_clustered; SELECT /* C_DATETIME - dt_comp_clus */ dt =CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) FROM dbo.datetime_compression_clustered; SELECT /* C_DATE - dt_comp_clus */ dt =CONVERT(DATE, dt) FROM dbo.datetime_compression_clustered; SELECT /* C_INT_FLOAT - dt_comp_clus */ dt =CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) FROM dbo.datetime_compression_clustered; SELECT /* DATEADD - dt_comp_clus */ dt =DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') FROM dbo.datetime_compression_clustered; SELECT /* RAW - dt_comp_clus */ dt FROM dbo.datetime_compression_clustered;Zaměříme-li se na výsledky pro tabulky s klastrovaným indexem, je jasné, že konverze do data byla velmi blízko k pouhému výběru nezpracovaných dat (kliknutím zvětšíte):
(Pro tuto sadu dotazů halda vykazovala velmi podobné výsledky – prakticky nerozeznatelné.)
Závěr
V případě, že jste chtěli přeskočit na pointu, tyto výsledky ukazují, že převody v paměti nejsou důležité, ale pokud převádíte data na cestě ven z tabulky (nebo jako součást predikátu vyhledávání), zvolená metoda může mít dramatický dopad na výkon. Převod na
DATE
(na jeden den) nebo použití neomezeného časového období v každém případě přinese nejlepší výkon, zatímco nejoblíbenější metoda – převod na řetězec – je naprosto propastná.Také vidíme, že komprese může mít slušný vliv na úložný prostor s velmi malým dopadem na výkon dotazů. Zdá se, že účinek na výkon vkládání závisí spíše na tom, zda má tabulka seskupený index, než na tom, zda je či není povolena komprese. Při použití seskupeného indexu však došlo k výraznému nárůstu doby, kterou trvalo vložení 10 milionů řádků. Něco, co je třeba mít na paměti a vyvážit úsporou místa na disku.
Je zřejmé, že by to mohlo zahrnovat mnohem více testování s podstatnějším a rozmanitějším pracovním zatížením, které mohu dále prozkoumat v budoucím příspěvku.