Så fort jag såg SQL 2016-funktionen AT TIME ZONE, som jag skrev om här på sqlperformance.com a för några månader sedan kom jag ihåg en rapport som behövde den här funktionen. Det här inlägget utgör en fallstudie om hur jag såg det fungera, vilket passar in i denna månads T-SQL-tisdag med Matt Gordon (@sqlatspeed). (Det är den 87:e T-SQL-tisdagen, och jag behöver verkligen skriva fler blogginlägg, särskilt om saker som inte föranleds av T-SQL-tisdagar.)
Situationen var denna, och det här kanske låter bekant om du läser mitt tidigare inlägg.
Långt innan LobsterPot Solutions fanns behövde jag ta fram en rapport om incidenter som inträffade, och i synnerhet visa hur många gånger som svar gjordes inom SLA och antalet gånger som SLA missades. Till exempel skulle en Sev2-incident som inträffade klockan 16:30 på en vardag behöva ha ett svar inom 1 timme, medan en Sev2-incident som inträffade klockan 17:30 på en vardag skulle behöva ha ett svar inom 3 timmar. Eller något i den stilen – jag glömmer siffrorna inblandade, men jag minns att helpdesk-anställda andades en suck när klockan 17 skulle rulla runt, eftersom de inte skulle behöva svara på saker så snabbt. De 15 minuter långa Sev1-varningarna skulle plötsligt sträcka sig till en timme och brådskan skulle försvinna.
Men ett problem skulle uppstå när sommartid började eller slutade.
Jag är säker på att om du har hanterat databaser kommer du att känna till smärtan som sommartid är. Förmodligen kom Ben Franklin på idén – och för det borde han bli träffad av blixten eller något. Western Australia provade det i några år nyligen och övergav det förnuftigt. Och den allmänna konsensus är att lagra datum/tid-data ska göra det i UTC.
Om du inte lagrar data i UTC riskerar du att ha ett evenemang som börjar 02:45 och slutar 02:15 efter att klockorna har gått tillbaka. Eller ha en SLA-incident som börjar 01:59 precis innan klockorna går framåt. Nu är dessa tider bra om du lagrar tidszonen som de är i, men i UTC-tid fungerar precis som förväntat.
…förutom rapportering.
För hur ska jag veta om ett visst datum var innan sommartid började eller efter? Jag kanske vet att en incident inträffade klockan 6:30 i UTC, men är det 16:30 i Melbourne eller 17:30? Självklart kan jag fundera på vilken månad det är, för jag vet att Melbourne har sommartid från första söndagen i oktober till första söndagen i april, men sedan om det finns kunder i Brisbane och Auckland, och Los Angeles och Phoenix, och på olika platser i Indiana blir saker mycket mer komplicerade.
För att komma runt detta fanns det väldigt få tidszoner där SLA kunde definieras för det företaget. Det ansågs bara vara för svårt att tillgodose mer än så. En rapport kan sedan anpassas för att säga "Tänk på att på ett visst datum ändrades tidszonen från X till Y". Det kändes rörigt, men det fungerade. Det behövdes inget för att slå upp Windows-registret, och det fungerade i princip bara.
Men nuförtiden skulle jag ha gjort det annorlunda.
Nu skulle jag ha använt AT TIME ZONE.
Du förstår, nu kunde jag lagra kundens tidszonsinformation som en egenskap för kunden. Jag kunde sedan lagra varje incidenttid i UTC, så att jag kan göra nödvändiga beräkningar kring antalet minuter att svara, lösa och så vidare, samtidigt som jag kan rapportera med kundens lokala tid. Om jag antar att min IncidentTime faktiskt hade lagrats med datetime, snarare än datetimeoffset, skulle det helt enkelt vara en fråga om att använda kod som:
i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE c.tz
…som först lägger den tidszonlösa i.IncidentTime till UTC, innan den konverteras till kundens tidszon. Och den här tidszonen kan vara "AUS Eastern Standard Time", eller "Mauritius Standard Time", eller vad som helst. Och SQL-motorn får ta reda på vilken offset som ska användas för det.
Vid det här laget kan jag mycket enkelt skapa en rapport som listar varje incident över en tidsperiod och visar den i kundens lokala tidszon. Jag kan konvertera värdet till tidsdatatypen och sedan rapportera mot hur många incidenter som var inom kontorstid eller inte.
Och allt detta är väldigt användbart, men hur är det med indexeringen för att hantera detta snyggt? När allt kommer omkring är AT TIME ZONE en funktion. Men att ändra tidszonen ändrar inte ordningen i vilken incidenterna faktiskt inträffade, så det borde vara okej.
För att testa detta skapade jag en tabell som heter dbo.Incidents och indexerade kolumnen IncidentTime. Sedan körde jag den här frågan och bekräftade att en indexsökning användes.
select i.IncidentTime, itz.LocalTime from dbo.Incidents i cross apply (select i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where i.IncidentTime >= '20170201' and i.IncidentTime < '20170301';
Men jag vill filtrera på itz.LocalTime...
select i.IncidentTime, itz.LocalTime from dbo.Incidents i cross apply (select i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where itz.LocalTime >= '20170201' and itz.LocalTime < '20170301';
Ingen tur. Det gillade inte indexet.
Varningarna beror på att det måste titta igenom mycket mer än den information jag är intresserad av.
Jag försökte till och med använda en tabell med ett datum- och tidsförskjutningsfält. När allt kommer omkring kan AT TIME ZONE ändra ordningen när du flyttar från datetime till datetimeoffset, även om ordningen inte ändras när du flyttar från datetimeoffset till en annan datetimeoffset. Jag försökte till och med se till att det jag jämförde med var i tidszonen.
select i.IncidentTime, itz.LocalTime from dbo.IncidentsOffset i cross apply (select i.IncidentTime AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where itz.LocalTime >= cast('20170201' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time' and itz.LocalTime < cast('20170301' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time';
Fortfarande ingen tur!
Så nu hade jag två alternativ. En var att lagra den konverterade versionen tillsammans med UTC-versionen och indexera den. Jag tycker att det är jobbigt. Det är verkligen mycket mer av en databasändring än jag skulle vilja.
Det andra alternativet var att använda vad jag kallar hjälparpredikat. Det här är sådana saker du ser när du använder LIKE. De är predikat som kan användas som sökpredikat, men inte exakt vad du efterfrågar.
Jag tror att oavsett vilken tidszon jag är intresserad av, är IncidentTimes som jag bryr mig om inom ett mycket specifikt intervall. Det intervallet är inte mer än en dag större än mitt föredragna intervall, på båda sidor.
Så jag tar med två extra predikat.
select i.IncidentTime, itz.LocalTime from dbo.IncidentsOffset i cross apply (select i.IncidentTime AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where itz.LocalTime >= cast('20170201' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time' and itz.LocalTime < cast('20170301' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time and i.IncidentTime >= dateadd(day,-1,'20170201') and i.IncidentTime < dateadd(day, 1,'20170301');
Nu kan mitt index användas. Den måste titta igenom 30 rader innan den filtreras till de 28 som den bryr sig om – men det är mycket bättre än att skanna det hela.
Och du vet – det här är den typen av beteende som jag ser hela tiden från vanliga frågor, som när jag gör CAST(myDateTimeColumns AS DATE) =@SomeDate, eller använder LIKE.
Jag är okej med detta. AT TIME ZONE är bra för att låta mig hantera mina tidszonsomvandlingar, och genom att överväga vad som händer med mina frågor behöver jag inte heller offra prestanda.
@rob_farley