Om du får några riktigt konstiga resultat när du använder DATEDIFF()
funktion i SQL Server, och du är övertygad om att funktionen innehåller en bugg, slit inte ur håret ännu. Det är förmodligen inte en bugg.
Det finns scenarier där resultaten som produceras av den här funktionen kan vara ganska skumma. Och om du inte förstår hur funktionen faktiskt fungerar kommer resultaten att se helt fel ut.
Förhoppningsvis kan den här artikeln hjälpa till att förtydliga hur DATEDIFF()
funktionen är utformad för att fungera och ger några exempel på scenarier där dina resultat kanske inte blir som du förväntar dig.
Exempel 1 – 365 dagar är inte alltid ett år
Fråga: När är 365 dagar inte ett år?
Svar: När du använder DATEDIFF()
självklart!
Här är ett exempel där jag använder DATEDIFF()
för att returnera antalet dagar mellan två datum och sedan antalet år mellan samma två datum.
DECLARE @startdate datetime2 = '2016-01-01 00:00:00.0000000', @enddate datetime2 = '2016-12-31 23:59:59.9999999'; SELECT DATEDIFF(day, @startdate, @enddate) Days, DATEDIFF(year, @startdate, @enddate) Years;
Resultat:
+--------+---------+ | Days | Years | |--------+---------| | 365 | 0 | +--------+---------+
Om du tror att det här resultatet är fel, och att DATEDIFF()
har uppenbarligen en bugg, läs vidare – allt är inte som det verkar.
Tro det eller ej, detta är faktiskt det förväntade resultatet. Detta resultat är exakt i enlighet med hur DATEDIFF()
är designad för att fungera.
Exempel 2 – 100 nanosekunder =1 år?
Låt oss ta det åt andra hållet.
DECLARE @startdate datetime2 = '2016-12-31 23:59:59.9999999', @enddate datetime2 = '2017-01-01 00:00:00.0000000'; SELECT DATEDIFF(year, @startdate, @enddate) Year, DATEDIFF(quarter, @startdate, @enddate) Quarter, DATEDIFF(month, @startdate, @enddate) Month, DATEDIFF(dayofyear, @startdate, @enddate) DOY, DATEDIFF(day, @startdate, @enddate) Day, DATEDIFF(week, @startdate, @enddate) Week, DATEDIFF(hour, @startdate, @enddate) Hour, DATEDIFF(minute, @startdate, @enddate) Minute, DATEDIFF(second, @startdate, @enddate) Second, DATEDIFF(millisecond, @startdate, @enddate) Millisecond, DATEDIFF(microsecond, @startdate, @enddate) Microsecond, DATEDIFF(nanosecond, @startdate, @enddate) Nanosecond;
Resultat (visas med vertikal utdata):
Year | 1 Quarter | 1 Month | 1 DOY | 1 Day | 1 Week | 1 Hour | 1 Minute | 1 Second | 1 Millisecond | 1 Microsecond | 1 Nanosecond | 100
Det skiljer bara hundra nanosekunder (0,0000001 sekund) mellan de två datumen/tiderna, men vi får exakt samma resultat för varje datumdel, utom nanosekunder.
Hur kan detta hända? Hur kan det vara 1 mikrosekunds skillnad och 1 års skillnad båda samtidigt? För att inte tala om alla datum där emellan?
Det kan verka galet, men det här är inte heller en bugg. Dessa resultat är exakt i enlighet med hur DATEDIFF()
ska fungera.
Och för att göra saker ännu mer förvirrande kan vi få olika resultat beroende på datatyp. Men vi kommer till det snart. Låt oss först titta på hur DATEDIFF()
funktionen fungerar faktiskt.
Den faktiska definitionen av DATEDIFF()
Anledningen till att vi får de resultat vi gör är att DATEDIFF()
funktionen definieras enligt följande:
Denna funktion returnerar antalet (som ett signerat heltalsvärde) för de angivna datumdelens gränser som korsas mellan det angivna startdatumet och slutdatum .
Var särskilt uppmärksam på orden "datepart gränser passerade". Det är därför vi får de resultat vi gör i de tidigare exemplen. Det är lätt att anta att DATEDIFF()
använder förfluten tid för sina beräkningar, men det gör det inte. Den använder antalet datumdelgränser som passerats.
I det första exemplet passerade inte datumen några gränser för år. Året för det första datumet var exakt detsamma som året för det andra datumet. Inga gränser passerades.
I det andra exemplet hade vi det motsatta scenariot. Datumen korsade varje datumdelgräns minst en gång (100 gånger för nanosekunder).
Exempel 3 – Ett annat resultat för veckan
Låt oss nu låtsas att ett helt år har gått. Och här är vi exakt ett år senare med datum/tid-värdena, förutom att årsvärdena har ökat med ett.
Vi borde få samma resultat, eller hur?
DECLARE @startdate datetime2 = '2017-12-31 23:59:59.9999999', @enddate datetime2 = '2018-01-01 00:00:00.0000000'; SELECT DATEDIFF(year, @startdate, @enddate) Year, DATEDIFF(quarter, @startdate, @enddate) Quarter, DATEDIFF(month, @startdate, @enddate) Month, DATEDIFF(dayofyear, @startdate, @enddate) DOY, DATEDIFF(day, @startdate, @enddate) Day, DATEDIFF(week, @startdate, @enddate) Week, DATEDIFF(hour, @startdate, @enddate) Hour, DATEDIFF(minute, @startdate, @enddate) Minute, DATEDIFF(second, @startdate, @enddate) Second, DATEDIFF(millisecond, @startdate, @enddate) Millisecond, DATEDIFF(microsecond, @startdate, @enddate) Microsecond, DATEDIFF(nanosecond, @startdate, @enddate) Nanosecond;
Resultat:
Year | 1 Quarter | 1 Month | 1 DOY | 1 Day | 1 Week | 0 Hour | 1 Minute | 1 Second | 1 Millisecond | 1 Microsecond | 1 Nanosecond | 100
Fel.
De flesta av dem är desamma, men denna gång returnerade veckan 0
.
Va?
Detta hände eftersom inmatningsdatumen har samma kalender vecka värden. Det råkade bara vara så att datumen som valts till exempel 2 hade olika kalenderveckovärden.
För att vara mer specifik, exempel 2 korsade gränserna mellan veckodelar från "2016-12-31" till "2017-01-01". Detta beror på att den sista veckan av 2016 slutade 2016-12-31, och den första veckan av 2017 började 2017-01-01 (söndag).
Men i exempel 3 började den första veckan av 2018 faktiskt på vårt startdatum 2017-12-31 (söndag). Vårt slutdatum, som är nästa dag, inföll inom samma vecka. Därför korsades inga veckodelgränser.
Detta förutsätter naturligtvis att söndagen är den första dagen i varje vecka. Som det visar sig är DATEDIFF()
funktionen gör anta att söndag är den första dagen i veckan. Den ignorerar till och med din SET DATEFIRST
inställning (denna inställning låter dig uttryckligen ange vilken dag som anses vara den första dagen i veckan). Microsofts resonemang för att ignorera SET DATEFIRST
är att den säkerställer DATEDIFF()
funktionen är deterministisk. Här är en lösning om detta är ett problem för dig.
Så i ett nötskal kan dina resultat se "fel" ut för vilken datumdel som helst beroende på datum/tider. Dina resultat kan se extra fel ut när du använder veckodelen. Och de kan se ännu mer fel ut om du använder en SET DATEFIRST
annat värde än 7 (för söndag) och du förväntar dig DATEDIFF()
att hedra det.
Men resultaten är inte fel, och det är inte en bugg. Det är bara mer av en "gotcha" för de som inte är medvetna om hur funktionen faktiskt fungerar.
Alla dessa gotchas gäller även för DATEDIFF_BIG()
fungera. Det fungerar på samma sätt som DATEDIFF()
med undantaget att det returnerar resultatet som en signerad bigint (i motsats till en int för DATEDIFF()
).
Exempel 4 – Resultat beror på datatyp
Du kan också få oväntade resultat på grund av den datatyp du använder för dina inmatningsdatum. Resultaten kommer ofta att skilja sig beroende på inmatningsdatumens datatyp. Men du kan inte skylla på DATEDIFF()
för detta, eftersom det enbart beror på de olika datatypernas möjligheter och begränsningar. Du kan inte förvänta dig att få resultat med hög precision från ett inmatningsvärde med låg precision.
Till exempel när startdatumet eller slutdatumet har en smalldatetime värde, kommer sekunderna och millisekunderna alltid att returnera 0. Detta beror på att smalldatetime datatypen är endast exakt på minut.
Här är vad som händer om vi byter exempel 2 för att använda smalldatetime istället för datetime2 :
DECLARE @startdate smalldatetime = '2016-12-31 23:59:59', @enddate smalldatetime = '2017-01-01 00:00:00'; SELECT DATEDIFF(year, @startdate, @enddate) Year, DATEDIFF(quarter, @startdate, @enddate) Quarter, DATEDIFF(month, @startdate, @enddate) Month, DATEDIFF(dayofyear, @startdate, @enddate) DOY, DATEDIFF(day, @startdate, @enddate) Day, DATEDIFF(week, @startdate, @enddate) Week, DATEDIFF(hour, @startdate, @enddate) Hour, DATEDIFF(minute, @startdate, @enddate) Minute, DATEDIFF(second, @startdate, @enddate) Second, DATEDIFF(millisecond, @startdate, @enddate) Millisecond, DATEDIFF(microsecond, @startdate, @enddate) Microsecond, DATEDIFF(nanosecond, @startdate, @enddate) Nanosecond;
Resultat:
Year | 0 Quarter | 0 Month | 0 DOY | 0 Day | 0 Week | 0 Hour | 0 Minute | 0 Second | 0 Millisecond | 0 Microsecond | 0 Nanosecond | 0
Anledningen till att alla är noll är för att båda inmatningsdatumen faktiskt är identiska:
DECLARE @startdate smalldatetime = '2016-12-31 23:59:59', @enddate smalldatetime = '2017-01-01 00:00:00'; SELECT @startdate 'Start Date', @enddate 'End Date';
Resultat:
+---------------------+---------------------+ | Start Date | End Date | |---------------------+---------------------| | 2017-01-01 00:00:00 | 2017-01-01 00:00:00 | +---------------------+---------------------+
Begränsningarna för smalldatetime datatypen gjorde att sekunderna avrundades uppåt, vilket sedan orsakade ett flöde på effekt och allt avrundades uppåt. Även om du inte får identiska indatavärden kan du fortfarande få ett oväntat resultat på grund av att datatypen inte ger den precision du behöver.