Så snart jeg så SQL 2016-funktionen AT TIME ZONE, som jeg skrev om her på sqlperformance.com a For et par måneder siden huskede jeg en rapport, der havde brug for denne funktion. Dette indlæg danner et casestudie om, hvordan jeg så det fungere, som passer ind i denne måneds T-SQL-tirsdag afholdt af Matt Gordon (@sqlatspeed). (Det er den 87. T-SQL tirsdag, og jeg har virkelig brug for at skrive flere blogindlæg, især om ting, der ikke er foranlediget af T-SQL tirsdage.)
Situationen var denne, og det lyder måske bekendt, hvis du læser mit tidligere indlæg.
Længe før LobsterPot Solutions eksisterede, havde jeg brug for at lave en rapport om hændelser, der opstod, og især vise antallet af gange, der blev givet svar inden for SLA, og antallet af gange, som SLA'en blev savnet. For eksempel skal en Sev2-hændelse, der fandt sted kl. 16.30 på en hverdag, have et svar inden for 1 time, mens en Sev2-hændelse, der fandt sted kl. 17.30 på en hverdag, skulle have et svar inden for 3 timer. Eller sådan noget – jeg glemmer tallene, men jeg kan huske, at helpdesk-medarbejderne åndede lettet op, når klokken 17 rullede rundt, fordi de ikke behøvede at reagere så hurtigt på tingene. De 15-minutters Sev1-alarmer ville pludselig strække sig til en time, og det hastede med at forsvinde.
Men et problem ville opstå, når sommertid startede eller sluttede.
Jeg er sikker på, at hvis du har beskæftiget dig med databaser, vil du kende den smerte, som sommertid er. Angiveligt kom Ben Franklin på ideen - og for det skulle han blive ramt af et lyn eller noget. Western Australia prøvede det i et par år for nylig og forlod det fornuftigt nok. Og den generelle konsensus er at gemme dato/tidsdata skal gøre det i UTC.
Hvis du ikke gemmer data i UTC, risikerer du, at en begivenhed starter kl. 02.45 og slutter kl. 02.15, efter at urene er gået tilbage. Eller har en SLA-hændelse, der starter kl. 01:59 lige før urene går fremad. Nu er disse tider fine, hvis du gemmer den tidszone, de er i, men i UTC-tid fungerer bare som forventet.
…undtagen rapportering.
For hvordan skal jeg vide, om en bestemt dato var før sommerferien startede eller efter? Jeg ved måske, at en hændelse fandt sted kl. 6.30 i UTC, men er det kl. 16.30 i Melbourne eller 17.30? Jeg kan selvfølgelig overveje, hvilken måned det er i, for jeg ved, at Melbourne holder sommertid fra den første søndag i oktober til den første søndag i april, men hvis der så er kunder i Brisbane og Auckland, og Los Angeles og Phoenix, og forskellige steder i Indiana bliver tingene meget mere komplicerede.
For at komme uden om dette var der meget få tidszoner, hvor SLA'er kunne defineres for den pågældende virksomhed. Det blev bare anset for svært at tage højde for mere end det. En rapport kunne derefter tilpasses til at sige "Tænk på, at tidszonen på en bestemt dato ændrede sig fra X til Y". Det føltes rodet, men det virkede. Der var ikke behov for noget for at slå op i Windows-registreringsdatabasen, og det virkede stort set bare.
Men i disse dage ville jeg have gjort det anderledes.
Nu ville jeg have brugt AT TIME ZONE.
Ser du, nu kunne jeg gemme kundens tidszoneoplysninger som en egenskab for kunden. Jeg kunne derefter gemme hver hændelsestid i UTC, så jeg kunne lave de nødvendige beregninger omkring antallet af minutter til at reagere, løse og så videre, samtidig med at jeg kunne rapportere ved hjælp af kundens lokale tid. Hvis vi antager, at min IncidentTime faktisk var blevet gemt ved hjælp af datetime, snarere end datetimeoffset, ville det simpelthen være et spørgsmål om at bruge kode som:
i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE c.tz
…hvilket først sætter den tidszoneløse i.IncidentTime i UTC, før den konverteres til kundens tidszone. Og denne tidszone kan være 'AUS Eastern Standard Time' eller 'Mauritius Standard Time' eller hvad som helst. Og SQL Engine er tilbage til at finde ud af, hvilken offset der skal bruges til det.
På dette tidspunkt kan jeg meget nemt oprette en rapport, der viser hver hændelse på tværs af en tidsperiode, og vise den i kundens lokale tidszone. Jeg kan konvertere værdien til tidsdatatypen og derefter rapportere mod, hvor mange hændelser der var inden for arbejdstiden eller ej.
Og alt dette er meget nyttigt, men hvad med indekseringen for at håndtere dette pænt? Når alt kommer til alt, er AT TIME ZONE en funktion. Men at ændre tidszonen ændrer ikke den rækkefølge, som hændelserne faktisk fandt sted i, så det burde være i orden.
For at teste dette oprettede jeg en tabel kaldet dbo.Incidents og indekserede IncidentTime-kolonnen. Så kørte jeg denne forespørgsel og bekræftede, at der blev brugt en indekssøgning.
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 jeg vil gerne filtrere 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';
Intet held. Det kunne ikke lide indekset.
Advarslerne skyldes, at den skal kigge meget mere igennem end de data, jeg er interesseret i.
Jeg prøvede endda at bruge en tabel med et felt for dato- og tidsforskydning. Når alt kommer til alt, kan AT TIME ZONE ændre rækkefølgen, når du flytter fra datetime til datetime offset, selvom rækkefølgen ikke ændres, når du flytter fra datetime offset til en anden datetime offset. Jeg prøvede endda at sikre mig, at det, jeg sammenlignede det 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';
Stadig uden held!
Så nu havde jeg to muligheder. Den ene var at gemme den konverterede version sammen med UTC-versionen og indeksere den. Jeg synes, det er en smerte. Det er bestemt meget mere en databaseændring, end jeg kunne tænke mig.
Den anden mulighed var at bruge det, jeg kalder hjælperprædikater. Det er den slags ting, du ser, når du bruger LIKE. Det er prædikater, der kan bruges som søgeprædikater, men ikke lige det, du beder om.
Jeg regner med, at uanset hvilken tidszone jeg er interesseret i, er IncidentTimes, som jeg holder af, inden for et meget specifikt interval. Det interval er ikke mere end en dag større end mit foretrukne udvalg, på begge sider.
Så jeg vil inkludere to ekstra prædikater.
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 mit indeks bruges. Den skal kigge gennem 30 rækker, før den filtrerer den til de 28, den bekymrer sig om - men det er meget bedre end at scanne det hele.
Og du ved – det er den slags adfærd, jeg ser hele tiden fra almindelige forespørgsler, som når jeg laver CAST(myDateTimeColumns AS DATE) =@SomeDate, eller bruger LIKE.
Jeg er okay med dette. AT TIME ZONE er fantastisk til at lade mig håndtere mine tidszonekonverteringer, og ved at overveje, hvad der sker med mine forespørgsler, behøver jeg heller ikke at ofre ydeevne.
@rob_farley