Tan pronto como vi la función de SQL 2016 AT TIME ZONE, sobre la que escribí aquí en sqlperformance.com, Hace unos meses, recordé un informe que necesitaba esta característica. Esta publicación forma un estudio de caso sobre cómo vi que funcionó, que encaja en el martes de T-SQL de este mes organizado por Matt Gordon (@sqlatspeed). (Es el martes 87 de T-SQL, y realmente necesito escribir más publicaciones de blog, en particular sobre cosas que no se solicitan en los martes de T-SQL).
La situación era esta, y esto puede sonar familiar si lees mi publicación anterior.
Mucho antes de que existiera LobsterPot Solutions, necesitaba producir un informe sobre los incidentes que ocurrían y, en particular, mostrar la cantidad de veces que se respondieron dentro del SLA y la cantidad de veces que se perdió el SLA. Por ejemplo, un incidente Sev2 que ocurrió a las 4:30 p. m. en un día laborable debería tener una respuesta dentro de 1 hora, mientras que un incidente Sev2 que ocurrió a las 5:30 p. m. en un día laborable debería tener una respuesta dentro de 3 horas. O algo así:olvidé los números involucrados, pero sí recuerdo que los empleados de la mesa de ayuda respiraban aliviados cuando llegaban las 5 p.m., porque no necesitaban responder a las cosas tan rápido. Las alertas Sev1 de 15 minutos se extenderían repentinamente a una hora y la urgencia desaparecería.
Pero surgiría un problema cada vez que comenzara o terminara el horario de verano.
Estoy seguro de que si ha tratado con bases de datos, sabrá lo doloroso que es el horario de verano. Supuestamente, a Ben Franklin se le ocurrió la idea, y por eso debería ser alcanzado por un rayo o algo así. Australia Occidental lo intentó durante algunos años recientemente y lo abandonó con sensatez. Y el consenso general es que almacenar datos de fecha/hora es hacerlo en UTC.
Si no almacena datos en UTC, corre el riesgo de que un evento comience a las 2:45 a. m. y finalice a las 2:15 a. m. después de que los relojes hayan retrocedido. O tener un incidente de SLA que comienza a la 1:59 a. m., justo antes de que se adelanten los relojes. Ahora, estos tiempos están bien si almacena la zona horaria en la que se encuentran, pero en el horario UTC funciona como se esperaba.
…excepto para informar.
Porque, ¿cómo se supone que voy a saber si una fecha en particular fue antes o después de que comenzara el horario de verano? Es posible que sepa que ocurrió un incidente a las 6:30 a. m. en UTC, pero ¿son las 4:30 p. m. en Melbourne o las 5:30 p. m.? Obviamente, puedo considerar en qué mes está, porque sé que Melbourne observa el horario de verano desde el primer domingo de octubre hasta el primer domingo de abril, pero si hay clientes en Brisbane, Auckland, Los Ángeles y Phoenix, y varios lugares dentro de Indiana, las cosas se vuelven mucho más complicadas.
Para evitar esto, había muy pocas zonas horarias en las que se pudieran definir SLA para esa empresa. Simplemente se consideró demasiado difícil atender más que eso. Luego, se podría personalizar un informe para que diga "Considere que en una fecha particular la zona horaria cambió de X a Y". Se sentía desordenado, pero funcionó. No hubo necesidad de nada para buscar el registro de Windows y básicamente funcionó.
Pero en estos días, lo habría hecho de otra manera.
Ahora, hubiera usado AT TIME ZONE.
Verá, ahora podría almacenar la información de la zona horaria del cliente como propiedad del cliente. Luego podría almacenar cada hora de incidente en UTC, lo que me permite hacer los cálculos necesarios sobre la cantidad de minutos para responder, resolver, etc., mientras puedo informar utilizando la hora local del cliente. Suponiendo que mi IncidentTime se haya almacenado usando datetime, en lugar de datetimeoffset, simplemente sería cuestión de usar un código como:
i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE c.tz
…que primero coloca el i.IncidentTime sin zona horaria en UTC, antes de convertirlo a la zona horaria del cliente. Y esta zona horaria puede ser 'Hora estándar del este de Australia', 'Hora estándar de Mauricio' o lo que sea. Y se deja que SQL Engine descubra qué compensación usar para eso.
En este punto, puedo crear muy fácilmente un informe que enumere cada incidente en un período de tiempo y mostrarlo en la zona horaria local del cliente. Puedo convertir el valor al tipo de datos de tiempo y luego informar sobre cuántos incidentes ocurrieron dentro del horario comercial o no.
Y todo esto es muy útil, pero ¿qué pasa con la indexación para manejar esto bien? Después de todo, AT TIME ZONE es una función. Pero cambiar la zona horaria no cambia el orden en que ocurrieron los incidentes, por lo que debería estar bien.
Para probar esto, creé una tabla llamada dbo.Incidents e indexé la columna IncidentTime. Luego ejecuté esta consulta y confirmé que se utilizó una búsqueda de índice.
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';
Pero quiero filtrar en 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';
Sin suerte. No le gustó el índice.
Las advertencias se deben a que tiene que analizar mucho más que los datos que me interesan.
Incluso intenté usar una tabla con un campo datetimeoffset. Después de todo, AT TIME ZONE puede cambiar el orden al pasar de datetime a datetimeoffset, aunque el orden no cambie al pasar de datetimeoffset a otro datetimeoffset. Incluso traté de asegurarme de que lo que estaba comparando estuviera en la zona horaria.
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';
Todavía sin suerte!
Así que ahora tenía dos opciones. Una era almacenar la versión convertida junto con la versión UTC e indexarla. Creo que eso es un dolor. Ciertamente es mucho más un cambio de base de datos de lo que me gustaría.
La otra opción era usar lo que yo llamo predicados auxiliares. Este es el tipo de cosas que ves cuando usas LIKE. Son predicados que se pueden usar como predicados de búsqueda, pero no exactamente lo que estás pidiendo.
Me imagino que no importa en qué zona horaria esté interesado, los IncidentTimes que me interesan están dentro de un rango muy específico. Ese rango no es más de un día más grande que mi rango preferido, en ambos lados.
Así que incluiré dos predicados adicionales.
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');
Ahora, mi índice puede ser usado. Tiene que mirar a través de 30 filas antes de filtrarlas a las 28 que le interesan, pero eso es mucho mejor que escanear todo.
Y ya sabe, este es el tipo de comportamiento que veo todo el tiempo en consultas regulares, como cuando hago CAST(myDateTimeColumns AS DATE) =@SomeDate, o uso LIKE.
Estoy bien con esto. AT TIME ZONE es excelente porque me permite manejar mis conversiones de zona horaria y, al considerar lo que sucede con mis consultas, tampoco necesito sacrificar el rendimiento.
@rob_farley