Non appena ho visto la funzionalità SQL 2016 AT TIME ZONE, di cui ho scritto qui su sqlperformance.com a qualche mese fa, mi sono ricordato di un rapporto che necessitava di questa funzione. Questo post costituisce un case study su come l'ho visto funzionare, che si inserisce nel T-SQL Tuesday di questo mese ospitato da Matt Gordon (@sqlatspeed). (È l'87° Martedì T-SQL e ho davvero bisogno di scrivere più post sul blog, in particolare su cose che non sono richieste dai Martedì T-SQL.)
La situazione era questa, e questo potrebbe suonare familiare se leggi quel mio precedente post.
Molto prima che esistessero le soluzioni LobsterPot, dovevo produrre un rapporto sugli incidenti che si sono verificati e, in particolare, mostrare il numero di volte in cui sono state fornite risposte all'interno dello SLA e il numero di volte in cui lo SLA è stato mancato. Ad esempio, un incidente Sev2 che si è verificato alle 16:30 di un giorno feriale dovrebbe avere una risposta entro 1 ora, mentre un incidente Sev2 che si è verificato alle 17:30 di un giorno feriale dovrebbe avere una risposta entro 3 ore. O qualcosa del genere:dimentico i numeri coinvolti, ma ricordo che i dipendenti dell'helpdesk tiravano un sospiro di sollievo quando le 17:00 sarebbero arrivate, perché non avrebbero bisogno di rispondere alle cose così rapidamente. Gli avvisi Sev1 di 15 minuti si estenderebbero improvvisamente a un'ora e l'urgenza scomparirebbe.
Ma un problema si presentava ogni volta che iniziava o terminava l'ora legale.
Sono sicuro che se hai avuto a che fare con i database, conoscerai il dolore che è l'ora legale. Presumibilmente Ben Franklin ha avuto l'idea - e per questo dovrebbe essere colpito da un fulmine o qualcosa del genere. L'Australia occidentale l'ha provato per alcuni anni di recente e l'ha sensatamente abbandonato. E il consenso generale è quello di archiviare i dati di data/ora in UTC.
Se non memorizzi i dati in UTC, corri il rischio che un evento inizi alle 2:45 e termini alle 2:15 dopo che gli orologi sono tornati indietro. O avere un incidente SLA che inizia all'1:59 appena prima che l'orologio vada avanti. Ora, questi orari vanno bene se memorizzi il fuso orario in cui si trovano, ma nell'ora UTC funziona come previsto.
...tranne per la segnalazione.
Perché come faccio a sapere se una data particolare era prima dell'inizio dell'ora legale o dopo? Potrei sapere che si è verificato un incidente alle 6:30 in UTC, ma sono le 16:30 a Melbourne o le 17:30? Ovviamente posso considerare in quale mese è, perché so che Melbourne osserva l'ora legale dalla prima domenica di ottobre alla prima domenica di aprile, ma poi se ci sono clienti a Brisbane, Auckland, Los Angeles e Phoenix, e in vari luoghi dell'Indiana, le cose si complicano molto.
Per aggirare questo problema, c'erano pochissimi fusi orari in cui potevano essere definiti SLA per quell'azienda. Era semplicemente considerato troppo difficile provvedere a qualcosa di più. Un rapporto potrebbe quindi essere personalizzato per dire "Si consideri che in una data particolare il fuso orario è cambiato da X a Y". Sembrava disordinato, ma ha funzionato. Non c'era bisogno di nulla per cercare il registro di Windows e praticamente funzionava.
Ma in questi giorni, l'avrei fatto diversamente.
Ora, avrei usato AT TIME ZONE.
Vedete, ora potrei memorizzare le informazioni sul fuso orario del cliente come proprietà del cliente. Potevo quindi memorizzare ogni orario di incidente in UTC, consentendomi di eseguire i calcoli necessari sul numero di minuti per rispondere, risolvere e così via, pur essendo in grado di segnalare utilizzando l'ora locale del cliente. Supponendo che il mio IncidentTime sia stato effettivamente archiviato utilizzando datetime, anziché datetimeoffset, si tratterebbe semplicemente di utilizzare codice come:
i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE c.tz
…che prima inserisce i.IncidentTime senza fuso orario in UTC, prima di convertirlo nel fuso orario del cliente. E questo fuso orario può essere "AUS Eastern Standard Time" o "Mauritius Standard Time" o qualsiasi altra cosa. E il motore SQL è lasciato a capire quale offset usare per quello.
A questo punto, posso creare facilmente un rapporto che elenca ogni incidente in un periodo di tempo e mostrarlo nel fuso orario locale del cliente. Posso convertire il valore nel tipo di dati dell'ora e quindi segnalare quanti incidenti si sono verificati durante l'orario lavorativo o meno.
E tutto questo è molto utile, ma per quanto riguarda l'indicizzazione per gestirlo bene? Dopotutto, AT TIME ZONE è una funzione. Ma cambiare il fuso orario non cambia l'ordine in cui si sono effettivamente verificati gli incidenti, quindi dovrebbe andare bene.
Per verificarlo, ho creato una tabella chiamata dbo.Incidents e ho indicizzato la colonna IncidentTime. Quindi ho eseguito questa query e ho confermato che era stata utilizzata una ricerca dell'indice.
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';
Ma voglio filtrare su 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';
Senza fortuna. Non mi è piaciuto l'indice.
Gli avvisi sono dovuti al fatto che deve esaminare molto di più dei dati che mi interessano.
Ho anche provato a utilizzare una tabella con un campo datetimeoffset. Dopotutto, AT TIME ZONE può modificare l'ordine quando si passa da datetime a datetimeoffset, anche se l'ordine non viene modificato quando si passa da datetimeoffset a un altro datetimeoffset. Ho anche provato ad assicurarmi che la cosa con cui lo stavo confrontando fosse nel fuso orario.
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';
Ancora nessuna fortuna!
Quindi ora avevo due opzioni. Uno era archiviare la versione convertita insieme alla versione UTC e indicizzarla. Penso che sia un dolore. È sicuramente molto più di una modifica del database di quanto vorrei.
L'altra opzione era usare quelli che chiamo predicati di supporto. Questo è il genere di cose che vedi quando usi LIKE. Sono predicati che possono essere usati come predicati di ricerca, ma non esattamente quello che stai chiedendo.
Immagino che, indipendentemente dal fuso orario che mi interessa, gli IncidentTimes a cui tengo si trovano all'interno di un intervallo molto specifico. Quella gamma non è più di un giorno più grande della mia gamma preferita, su entrambi i lati.
Quindi includerò due predicati extra.
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');
Ora, il mio indice può essere utilizzato. È necessario esaminare 30 righe prima di filtrarle nelle 28 a cui tiene, ma è molto meglio che scansionare l'intera cosa.
E sai:questo è il tipo di comportamento che vedo sempre dalle query regolari, come quando eseguo CAST(myDateTimeColumns AS DATE) =@SomeDate o uso LIKE.
Sto bene con questo. AT TIME ZONE è ottimo per permettermi di gestire le conversioni del mio fuso orario e, considerando cosa sta succedendo con le mie domande, non ho nemmeno bisogno di sacrificare le prestazioni.
@rob_farley