sql >> Databasteknik >  >> RDS >> Database

Vilket är det mest effektiva sättet att trimma tiden från datetime?

Det vanligaste behovet av att ta bort tid från ett datetime-värde är att få alla rader som representerar beställningar (eller besök eller olyckor) som inträffade en viss dag. Men inte alla tekniker som används för att göra det är effektiva eller ens säkra.

TL;DR-version

Om du vill ha en säker intervallfråga som fungerar bra, använd ett öppet intervall eller, för endagsfrågor på SQL Server 2008 och senare, använd CONVERT(DATE) :

DECLARE @idag DATETIME; -- endast <=2005:SET @idag =DATEADD(DAY, DATEDIFF(DAY, '20000101', CURRENT_TIMESTAMP), '20000101'); -- eller 2008 och uppåt:SET @idag =KONVERTERA(DATE, CURRENT_TIMESTAMP); -- och använd sedan ett öppet intervall i frågan:...WHERE OrderDate>=@today AND OrderDate  

Några varningar:

  • Var försiktig med DATEDIFF tillvägagångssätt, eftersom det finns några anomalier vid uppskattning av kardinalitet som kan uppstå (se det här blogginlägget och Stack Overflow-frågan som sporrade det för mer information).
  • Medan den sista fortfarande potentiellt kommer att använda en indexsökning (till skillnad från alla andra icke-sargerbara uttryck jag någonsin stött på), måste du vara försiktig med att konvertera kolumnen till ett datum innan du jämför. Även detta tillvägagångssätt kan ge fundamentalt felaktiga kardinalitetsuppskattningar. Se detta svar av Martin Smith för mer information.

I vilket fall som helst, läs vidare för att förstå varför dessa är de enda två metoderna jag någonsin rekommenderar.

Alla inflygningar är inte säkra

Som ett osäkert exempel ser jag att den här används mycket:

WHERE OrderDate MELLAN DATEDIFF(DAY, 0, GETDATE()) OCH DATEADD(MILLISECOND, -3, DATEDIFF(DAY, 0, GETDATE()) + 1);

Det finns några problem med detta tillvägagångssätt, men det mest anmärkningsvärda är beräkningen av dagens "slut" – om den underliggande datatypen är SMALLDATETIME , det slutintervallet kommer att avrunda uppåt; om det är DATETIME2 , kan du teoretiskt missa data i slutet av dagen. Om du väljer minuter eller nanosekunder eller någon annan lucka för att tillgodose den aktuella datatypen, kommer din fråga att börja ha konstigt beteende om datatypen någonsin skulle ändras senare (och låt oss vara ärliga, om någon ändrar kolumntypen till att vara mer eller mindre granulär, de springer inte runt och kontrollerar varje enskild fråga som kommer åt den). Att behöva koda på detta sätt beroende på typen av datum/tid-data i den underliggande kolumnen är fragmenterad och felbenägen. Det är mycket bättre att använda öppna datumintervall för detta:

Jag pratar mycket mer om detta i ett par gamla blogginlägg:

  • Vad har BETWEEN och djävulen gemensamt?
  • Dåliga vanor att sparka:felaktig hantering av datum-/intervallfrågor

Men jag ville jämföra prestandan för några av de vanligare tillvägagångssätten jag ser där ute. Jag har alltid använt öppna intervall och sedan SQL Server 2008 har vi kunnat använda CONVERT(DATE) och fortfarande använda ett index på den kolumnen, vilket är ganska kraftfullt.

VÄLJ KONVERTERA(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)));,SELECT CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, CURRENT_TIMESTAMP)));,SELECT CONVERT(DATETIME) CONVERT(FLOAT, CURRENT_TIMESTAMP)));

Ett enkelt prestandatest

För att utföra ett mycket enkelt initialt prestandatest, gjorde jag följande för vart och ett av ovanstående påståenden, och satte en variabel till utdata från beräkningen 100 000 gånger:

VÄLJ SYSDATETIME();GO DECLARE @d DATETIME =[konverteringsmetod];GO 100000 SELECT SYSDATETIME();GO

Jag gjorde detta tre gånger för varje metod, och de körde alla i intervallet 34-38 sekunder. Så strängt taget finns det mycket försumbara skillnader i dessa metoder när man utför operationerna i minnet:

Ett mer utarbetat prestandatest

Jag ville också jämföra dessa metoder med olika datatyper (DATETIME , SMALLDATETIME och DATETIME2 ), mot både ett klustrat index och en heap, och med och utan datakomprimering. Så först skapade jag en enkel databas. Genom experiment bestämde jag mig för att den optimala storleken för att hantera 120 miljoner rader och all loggaktivitet som kan uppstå (och för att förhindra att automatiskt växande händelser stör testningen) var en datafil på 20 GB och en logg på 3 GB:

SKAPA DATABAS [Datetime_Testing]PÅ PRIMÄR ( NAME =N'Datetime_Testing_Data', FILENAME =N'D:\DATA\Datetime_Testing.mdf', SIZE =20480000KB , MAXSIZE =UNBEGRÄNSAT) =FILEB GROOT 4TH, =0 'Datetime_Testing_Log', FILENAME =N'E:\LOGS\Datetime_Testing_log.ldf', SIZE =3000000KB, MAXSIZE =UNLIMITED, FILEGROWTH =20480KB);

Därefter skapade jag 12 tabeller:

-- klustrade index utan komprimering:CREATE TABLE dbo.smalldatetime_nocompression_clustered(dt SMALLDATETIME);CREATE CLUSTERED INDEX x PÅ dbo.smalldatetime_nocompression_clustered(dt); -- heap utan komprimering:CREATE TABLE dbo.smalldatetime_nocompression_heap(dt SMALLDATETIME); -- klustrat index med sidkomprimering: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); -- heap med sidkomprimering:CREATE TABLE dbo.smalldatetime_compression_heap(dt SMALLDATETIME)WITH (DATA_COMPRESSION =PAGE);

[Upprepa sedan igen för DATETIME och DATETIME2.]

Därefter infogade jag 10 000 000 rader i varje tabell. Jag gjorde detta genom att skapa en vy som skulle generera samma 10 000 000 datum varje gång:

SKAPA VY dbo.TenMillionDatesAS SELECT TOP (10000000) d =DATEADD(MINUT, ROW_NUMBER() ÖVER (ORDER BY s1.[object_id]), '19700101') FRÅN sys.all_columns AS s1 s1 CROSS.2OIN BESTÄLL EFTER s1.[object_id];

Detta gjorde att jag kunde fylla i tabellerna på detta sätt:

INSERT /* dt_comp_clus */ dbo.datetime_compression_clustered(dt) SELECT CONVERT(DATETIME, d) FROM dbo.TenMillionDates;CHECKPOINT;INSERT /* dt2_comp_clus */ dbo.datetime2_compression_clustered(dt) FROMTIMEbo, d)DATEBO .TenMillionDates;CHECKPOINT;INSERT /* sdt_comp_clus */ dbo.smalldatetime_compression_clustered(dt) SELECT CONVERT(SMALLDATETIME, d) FROM dbo.TenMillionDates;CHECKPOINT;

[Upprepa sedan igen för högarna och det okomprimerade klustrade indexet. Jag sätter en CHECKPOINT mellan varje infogning för att säkerställa loggåteranvändning (återställningsmodellen är enkel).]

INSERT Timings &Space Used

Här är tiderna för varje infogning (som fångat med Plan Explorer):

Och här är mängden utrymme som upptas av varje bord:

VÄLJ [tabell] =OBJECT_NAME([object_id]), row_count, page_count =reserved_page_count, reserved_size_MB =reserved_page_count * 8/1024FROM sys.dm_db_partition_stats WHERE OBJECT_NAME([object_id date) %LIKE; 

Frågemönsterprestanda

Därefter började jag testa två olika frågemönster för prestanda:

  • Räkna raderna för en specifik dag med hjälp av ovanstående sju tillvägagångssätt, såväl som det öppna datumintervallet
  • Konvertera alla 10 000 000 rader med ovanstående sju metoder, samt bara returnera rådata (eftersom formatering på klientsidan kan vara bättre)

[Med undantag för FLOAT metoder och DATETIME2 kolumnen, eftersom denna konvertering inte är laglig.]

För den första frågan ser frågorna ut så här (upprepas för varje tabelltyp):

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'; VÄLJ /* RANGE - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE dt>='19860301' OCH dt <'19860302';

Resultaten mot ett klustrat index ser ut så här (klicka för att förstora):

Här ser vi att konverteringen till datum och det öppna intervallet som använder ett index är de bästa resultaten. Men mot en hög tar konverteringen till datum faktiskt lite tid, vilket gör det öppna intervallet till det optimala valet (klicka för att förstora):

Och här är den andra uppsättningen av frågor (igen, upprepande för varje tabelltyp):

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; VÄLJ /* 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; VÄLJ /* 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;

Med fokus på resultaten för tabeller med ett klustrat index, är det tydligt att konverteringen hittills var mycket nära att bara välja rådata (klicka för att förstora):

(För den här uppsättningen frågor visade högen mycket liknande resultat – praktiskt taget omöjliga att skilja.)

Slutsats

Om du vill hoppa till punchline visar dessa resultat att konverteringar i minnet inte är viktiga, men om du konverterar data på väg ut ur en tabell (eller som en del av ett sökpredikat), kan metoden du väljer ha en dramatisk inverkan på prestandan. Konverterar till en DATE (för en enda dag) eller att använda ett öppet datumintervall i vilket fall som helst kommer att ge den bästa prestandan, medan den mest populära metoden där ute – att konvertera till en sträng – är helt urusel.

Vi ser också att komprimering kan ha en anständig effekt på lagringsutrymme, med mycket liten inverkan på frågeprestanda. Effekten på infogningsprestandan verkar vara lika beroende av om tabellen har ett klustrat index eller inte, snarare än om komprimering är aktiverad eller inte. Men med ett klustrat index på plats, var det en märkbar bula i varaktigheten det tog att infoga 10 miljoner rader. Något att tänka på och balansera med diskutrymmesbesparingar.

Det är uppenbart att det kan vara mycket mer testning inblandat, med mer omfattande och varierande arbetsbelastningar, som jag kan utforska ytterligare i ett framtida inlägg.


  1. HikariCP:Vilka tidsgränser på databasnivå bör övervägas för att ställa in maxLifetime för Oracle 11g

  2. postgreSQL - in vs any

  3. Hur mycket storlek Null-värdet tar i SQL Server

  4. SQL:Välj dynamiskt kolumnnamn baserat på variabel