Dès que j'ai vu la fonctionnalité SQL 2016 AT TIME ZONE, dont j'ai parlé ici sur sqlperformance.com, un il y a quelques mois, je me suis souvenu d'un rapport qui avait besoin de cette fonctionnalité. Ce message constitue une étude de cas sur la façon dont je l'ai vu fonctionner, qui s'inscrit dans le mardi T-SQL de ce mois-ci organisé par Matt Gordon (@sqlatspeed). (C'est le 87e mardi T-SQL, et j'ai vraiment besoin d'écrire plus d'articles de blog, en particulier sur les choses qui ne sont pas demandées par les mardis T-SQL.)
La situation était la suivante, et cela peut sembler familier si vous lisez mon message précédent.
Bien avant que LobsterPot Solutions n'existe, j'avais besoin de produire un rapport sur les incidents survenus, et en particulier, de montrer le nombre de fois où des réponses ont été apportées dans le cadre du SLA et le nombre de fois où le SLA a été manqué. Par exemple, un incident Sev2 qui s'est produit à 16 h 30 un jour de semaine devrait avoir une réponse dans un délai d'une heure, tandis qu'un incident Sev2 qui s'est produit à 17 h 30 un jour de semaine devrait avoir une réponse dans un délai de 3 heures. Ou quelque chose comme ça - j'oublie les chiffres impliqués, mais je me souviens que les employés du service d'assistance poussaient un soupir de soulagement à 17 heures, car ils n'auraient pas besoin de répondre si rapidement. Les alertes Sev1 de 15 minutes s'étendraient soudainement à une heure et l'urgence disparaîtrait.
Mais un problème survenait chaque fois que l'heure d'été commençait ou se terminait.
Je suis sûr que si vous avez eu affaire à des bases de données, vous saurez à quel point l'heure d'été est douloureuse. Soi-disant Ben Franklin a eu l'idée - et pour cela, il devrait être frappé par la foudre ou quelque chose comme ça. L'Australie occidentale l'a essayé pendant quelques années récemment, et l'a raisonnablement abandonné. Et le consensus général est de stocker les données de date/heure pour le faire en UTC.
Si vous ne stockez pas les données en UTC, vous courez le risque qu'un événement commence à 2h45 et se termine à 2h15 après le retour des horloges. Ou avoir un incident SLA qui commence à 1 h 59 juste avant que les horloges n'avancent. Maintenant, ces heures sont correctes si vous stockez le fuseau horaire dans lequel elles se trouvent, mais l'heure UTC fonctionne comme prévu.
… sauf pour les rapports.
Parce que comment suis-je censé savoir si une date particulière était avant ou après le début de l'heure d'été ? Je sais peut-être qu'un incident s'est produit à 6h30 à UTC, mais est-ce 16h30 à Melbourne ou 17h30 ? Évidemment, je peux tenir compte du mois, car je sais que Melbourne observe l'heure d'été du premier dimanche d'octobre au premier dimanche d'avril, mais s'il y a des clients à Brisbane, Auckland, Los Angeles et Phoenix, et divers endroits de l'Indiana, les choses deviennent beaucoup plus compliquées.
Pour contourner ce problème, il y avait très peu de fuseaux horaires dans lesquels des SLA pouvaient être définis pour cette entreprise. Il était juste considéré comme trop difficile de répondre à plus que cela. Un rapport pourrait alors être personnalisé pour indiquer « Considérez qu'à une date particulière, le fuseau horaire est passé de X à Y ». C'était désordonné, mais cela a fonctionné. Il n'y avait pas besoin de quoi que ce soit pour rechercher le registre Windows, et cela a fonctionné.
Mais ces jours-ci, j'aurais fait les choses différemment.
Maintenant, j'aurais utilisé AT TIME ZONE.
Vous voyez, maintenant je pourrais stocker les informations de fuseau horaire du client en tant que propriété du client. Je pourrais alors stocker chaque heure d'incident en UTC, ce qui me permettrait de faire les calculs nécessaires autour du nombre de minutes pour répondre, résoudre, etc., tout en étant en mesure de signaler en utilisant l'heure locale du client. En supposant que mon IncidentTime ait été stocké en utilisant datetime, plutôt que datetimeoffset, il s'agirait simplement d'utiliser un code comme :
i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE c.tz
… qui met d'abord le i.IncidentTime sans fuseau horaire en UTC, avant de le convertir dans le fuseau horaire du client. Et ce fuseau horaire peut être 'AUS Eastern Standard Time', ou 'Mauritius Standard Time', ou autre. Et le moteur SQL doit déterminer quel décalage utiliser pour cela.
À ce stade, je peux très facilement créer un rapport qui répertorie chaque incident sur une période donnée et l'afficher dans le fuseau horaire local du client. Je peux convertir la valeur en type de données temporelles, puis signaler le nombre d'incidents survenus ou non pendant les heures ouvrables.
Et tout cela est très utile, mais qu'en est-il de l'indexation pour bien gérer cela ? Après tout, AT TIME ZONE est une fonction. Mais changer le fuseau horaire ne change pas l'ordre dans lequel les incidents se sont réellement produits, donc ça devrait aller.
Pour tester cela, j'ai créé une table appelée dbo.Incidents et indexé la colonne IncidentTime. Ensuite, j'ai exécuté cette requête et confirmé qu'une recherche d'index avait été utilisée.
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';
Mais je veux filtrer sur 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';
Pas de chance. Il n'a pas aimé l'index.
Les avertissements sont dus au fait qu'il faut parcourir bien plus que les données qui m'intéressent.
J'ai même essayé d'utiliser une table avec un champ datetimeoffset. Après tout, AT TIME ZONE peut modifier l'ordre lors du passage de datetime à datetimeoffset, même si l'ordre n'est pas modifié lors du passage de datetimeoffset à un autre datetimeoffset. J'ai même essayé de m'assurer que la chose à laquelle je comparais était dans le fuseau horaire.
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';
Toujours pas de chance !
Alors maintenant, j'avais deux options. L'une consistait à stocker la version convertie à côté de la version UTC et à l'indexer. Je pense que c'est une douleur. C'est certainement beaucoup plus un changement de base de données que je ne le souhaiterais.
L'autre option consistait à utiliser ce que j'appelle des prédicats d'assistance. C'est le genre de chose que vous voyez lorsque vous utilisez LIKE. Ce sont des prédicats qui peuvent être utilisés comme Seek Predicates, mais pas exactement ce que vous demandez.
Je suppose que quel que soit le fuseau horaire qui m'intéresse, les IncidentTimes qui m'intéressent se situent dans une plage très spécifique. Cette plage n'est pas plus d'un jour plus grande que ma plage préférée, de chaque côté.
Je vais donc inclure deux prédicats supplémentaires.
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');
Maintenant, mon index peut être utilisé. Il doit parcourir 30 lignes avant de le filtrer sur les 28 qui l'intéressent - mais c'est bien mieux que de scanner le tout.
Et vous savez - c'est le genre de comportement que je vois tout le temps à partir de requêtes régulières, comme quand je fais CAST(myDateTimeColumns AS DATE) =@SomeDate, ou utiliser LIKE.
Je suis d'accord avec ça. AT TIME ZONE est idéal pour me permettre de gérer mes conversions de fuseau horaire, et en tenant compte de ce qui se passe avec mes requêtes, je n'ai pas non plus besoin de sacrifier les performances.
@rob_farley