Nous essayons d'optimiser une conception d'entrepôt de données qui prendra en charge la génération de rapports sur les données pour de nombreux fuseaux horaires. Par exemple, nous pourrions avoir un rapport pour la valeur d'un mois d'activité (millions de lignes) qui doit montrer l'activité groupée par heure de la journée. Et bien sûr, cette heure de la journée doit être l'heure "locale" pour le fuseau horaire donné.
Nous avions une conception qui fonctionnait bien lorsque nous venions de prendre en charge UTC et une heure locale. La conception standard des dimensions de date et d'heure pour UTC et l'heure locale, id sur les tables de faits. Cependant, cette approche ne semble pas évoluer si nous devons prendre en charge les rapports pour plus de 100 fuseaux horaires.
Nos tableaux de faits deviendraient très larges. En outre, nous devons résoudre le problème de syntaxe dans SQL consistant à spécifier les identifiants de date et d'heure à utiliser pour le regroupement sur une exécution donnée du rapport. Peut-être une très grosse déclaration CASE?
J'ai vu quelques suggestions pour obtenir toutes les données selon la plage de temps UTC que vous couvrez, puis les retourner à la couche de présentation pour les convertir en locales et les agréger, mais des tests limités avec SSRS suggèrent que ce sera extrêmement lent.
J'ai également consulté quelques livres sur le sujet, et ils semblent tous dire simplement avoir UTC et convertir en exposition ou avoir UTC et un local. J'apprécierais toutes vos pensées et suggestions.
Remarque: Cette question est similaire à: Gestion des fuseaux horaires dans le magasin de données / entrepôt , mais je ne peux pas faire de commentaire sur cette question, j'ai donc estimé que cela méritait sa propre question.
Mise à jour: J'ai sélectionné la réponse d'Aaron après qu'il ait fait des mises à jour importantes et publié des exemples de code et de diagrammes. Mes commentaires précédents sur sa réponse n'auront plus beaucoup de sens car ils faisaient référence à la modification originale de la réponse. Je vais essayer de revenir et de le mettre à jour si cela est justifié
Réponses:
J'ai résolu ce problème en ayant un tableau de calendrier très simple - chaque année a une ligne par fuseau horaire pris en charge , avec le décalage standard et le datetime de début / datetime de fin de l'heure d'été et son décalage (si ce fuseau horaire le prend en charge). Ensuite, une fonction en ligne, liée au schéma et de valeur de table qui prend le temps source (en UTC bien sûr) et ajoute / soustrait le décalage.
Cela ne fonctionnera évidemment jamais extrêmement bien si vous effectuez des rapports sur une grande partie des données; le partitionnement peut sembler utile, mais vous aurez toujours des cas où les dernières heures d'une année ou les premières heures de l'année suivante appartiennent en fait à une année différente lors de la conversion dans un fuseau horaire spécifique - de sorte que vous ne pourrez jamais obtenir la vraie partition l'isolement, sauf lorsque votre plage de rapports n'inclut pas le 31 décembre ou le 1er janvier.
Il y a quelques cas étranges que vous devez considérer:
2014-11-02 05:30 UTC et 2014-11-02 06:30 UTC se convertissent tous les deux à 01:30 AM dans le fuseau horaire de l'Est, par exemple (un pour la première fois 01:30 a été touché localement, puis un pour la deuxième fois lorsque les horloges ont reculé de 2h00 à 1h00 et une autre demi-heure s'est écoulée). Vous devez donc décider comment gérer cette heure de génération de rapports - selon UTC, vous devriez voir doubler le trafic ou le volume de tout ce que vous mesurez une fois que ces deux heures sont mappées sur une seule heure dans un fuseau horaire respectant l'heure d'été. Cela peut également jouer à des jeux amusants avec le séquencement des événements, car quelque chose qui devait logiquement se produire après que quelque chose d'autre puisse apparaîtrese produire avant lui une fois le calendrier réglé à une seule heure au lieu de deux. Un exemple extrême est une vue de page qui s'est produite à 05:59 UTC, puis un clic qui s'est produit à 06:00 UTC. En heure UTC, ces événements se sont produits à une minute d'intervalle, mais lorsqu'ils ont été convertis en heure de l'Est, la vue s'est produite à 1 h 59 du matin et le clic s'est produit une heure plus tôt.
2014-03-09 02:30 n'arrive jamais aux USA. En effet, à 2 heures du matin, nous faisons avancer les horloges à 3 heures du matin. Il est donc probable que vous souhaitiez générer une erreur si l'utilisateur entre une telle heure et vous demande de la convertir en UTC ou de concevoir votre formulaire afin que les utilisateurs ne puissent pas choisir une telle heure.
Même avec ces cas marginaux à l'esprit, je pense toujours que vous avez la bonne approche: stocker les données en UTC. Il est beaucoup plus facile de mapper des données vers d'autres fuseaux horaires à partir de l'UTC que d'un fuseau horaire vers un autre fuseau horaire, en particulier lorsque différents fuseaux horaires commencent / terminent l'heure d'été à différentes dates, et même le même fuseau horaire peut changer en utilisant des règles différentes au cours des différentes années ( par exemple, les États-Unis ont modifié les règles il y a environ 6 ans).
Vous voudrez utiliser une table de calendrier pour tout cela, pas une
CASE
expression gargantuesque (pas une déclaration ). Je viens d'écrire une série en trois parties pour MSSQLTips.com à ce sujet; Je pense que la 3e partie vous sera la plus utile:http://www.mssqltips.com/sqlservertip/3173/handle-conversion-between-time-zones-in-sql-server--part-1/
http://www.mssqltips.com/sqlservertip/3174/handle-conversion-between-time-zones-in-sql-server--part-2/
http://www.mssqltips.com/sqlservertip/3175/handle-conversion-between-time-zones-in-sql-server--part-3/
Un vrai exemple en direct, en attendant
Disons que vous avez un tableau de faits très simple. Le seul fait dont je me soucie dans ce cas est l'heure de l'événement, mais j'ajouterai un GUID vide de sens juste pour rendre la table suffisamment large pour que cela soit important. Encore une fois, pour être explicite, la table de faits stocke les événements en temps UTC et en temps UTC uniquement. J'ai même suffixé la colonne
_UTC
pour qu'il n'y ait pas de confusion.Maintenant, chargeons notre table de faits avec 10000000 lignes - représentant toutes les 3 secondes (1200 lignes par heure) du 30/12/2013 à minuit UTC jusqu'à quelque temps après 5 h 00 UTC le 12/12/2014. Cela garantit que les données chevauchent une limite annuelle, ainsi que l'heure d'été en avant et en arrière pour plusieurs fuseaux horaires. Cela semble vraiment effrayant, mais a pris environ 9 secondes sur mon système. Le tableau devrait finir par être d'environ 325 Mo.
Et juste pour montrer à quoi ressemblera une requête de recherche typique par rapport à cette table de lignes de 10 mm, si j'exécute cette requête:
J'obtiens ce plan, et il revient en 25 millisecondes *, faisant 358 lectures, pour retourner 72 totaux horaires:
* Durée mesurée par notre explorateur de plans SQL Sentry gratuit , qui ignore les résultats, donc cela n'inclut pas le temps de transfert réseau des données, le rendu, etc. En tant que clause de non-responsabilité supplémentaire, je travaille pour SQL Sentry.
Cela prend un peu plus de temps, évidemment, si je fais ma plage trop grande - un mois de données prend 258 ms, deux mois prend plus de 500 ms, et ainsi de suite. Le parallélisme peut entrer en jeu:
C'est là que vous commencez à penser à d'autres solutions meilleures pour satisfaire les requêtes de rapports, et cela n'a rien à voir avec le fuseau horaire que votre sortie affichera. Je n'entrerai pas dans les détails, je veux juste démontrer que la conversion de fuseau horaire ne va pas vraiment faire en sorte que vos requêtes de rapports soient beaucoup plus sujettes, et elles peuvent déjà le faire si vous obtenez de grandes plages qui ne sont pas prises en charge par une bonne index. Je vais m'en tenir à de petites plages de dates pour montrer que la logique est correcte et vous permettre de vous assurer que vos requêtes de rapports basées sur des plages fonctionnent correctement, avec ou sans conversion de fuseau horaire.
D'accord, nous avons maintenant besoin de tableaux pour stocker nos fuseaux horaires (avec décalages, en minutes, car tout le monde n'a même pas d'heures de décalage UTC) et les dates de changement d'heure d'été pour chaque année prise en charge. Par souci de simplicité, je ne vais entrer que quelques fuseaux horaires et une seule année pour faire correspondre les données ci-dessus.
Inclus quelques fuseaux horaires pour la variété, certains avec des décalages d'une demi-heure, certains qui n'observent pas l'heure d'été. Notez que l'Australie, dans l'hémisphère sud, observe l'heure d'été pendant notre hiver, donc leurs horloges remontent en avril et avancent en octobre. (Le tableau ci-dessus renverse les noms, mais je ne sais pas comment rendre cela moins déroutant pour les fuseaux horaires de l'hémisphère sud.)
Maintenant, une table de calendrier pour savoir quand les TZ changent. Je vais seulement insérer des lignes d'intérêt (chaque fuseau horaire ci-dessus, et seuls les changements d'heure d'été pour 2014). Pour faciliter les calculs dans les deux sens, je stocke à la fois le moment en UTC où un fuseau horaire change et le même moment dans l'heure locale. Pour les fuseaux horaires qui n'observent pas l'heure d'été, il est standard toute l'année et l'heure d'été "démarre" le 1er janvier.
Vous pouvez certainement remplir cela avec des algorithmes (et la prochaine série de conseils utilise des techniques intelligentes basées sur des ensembles, si je le dis moi-même), plutôt que de boucler, de remplir manuellement, qu'avez-vous. Pour cette réponse, j'ai décidé de remplir manuellement un an pour les cinq fuseaux horaires, et je ne vais pas déranger d'astuces fantaisistes.
D'accord, nous avons donc nos données factuelles et nos tableaux de "dimensions" (je grince des dents quand je dis cela), alors quelle est la logique? Eh bien, je suppose que les utilisateurs vont sélectionner leur fuseau horaire et entrer la plage de dates pour la requête. Je suppose également que la plage de dates sera de jours entiers dans leur propre fuseau horaire; pas de jours partiels, peu importe les heures partielles. Ils passeront donc une date de début, une date de fin et un TimeZoneID. À partir de là, nous utiliserons une fonction scalaire pour convertir la date de début / fin de ce fuseau horaire en UTC, ce qui nous permettra de filtrer les données en fonction de la plage UTC. Une fois que nous avons fait cela, et effectué nos agrégations dessus, nous pouvons ensuite appliquer la conversion des temps groupés au fuseau horaire source, avant de l'afficher à l'utilisateur.
L'UDF scalaire:
Et la fonction table:
Et une procédure qui l'utilise ( édition : mise à jour pour gérer le regroupement de décalages de 30 minutes):
(Vous voudrez peut-être essayer de court-circuiter là-bas, ou une procédure stockée distincte, dans le cas où l'utilisateur souhaite signaler en UTC - la traduction vers et depuis UTC va évidemment être un travail fastidieux.)
Exemple d'appel:
Retourne en 41ms *, et génère ce plan:
* Encore une fois, avec des résultats rejetés.
Pendant 2 mois, il revient en 507ms, et le plan est identique à part les rowcounts:
Bien que légèrement plus complexe et augmentant un peu le temps d'exécution, je suis assez confiant que ce type d'approche fonctionnera beaucoup, beaucoup mieux que l'approche de la table de bridge. Et ceci est un exemple instantané pour une réponse dba.se; Je suis sûr que ma logique et mon efficacité pourraient être améliorées par des gens beaucoup plus intelligents que moi.
Vous pouvez parcourir les données pour voir les cas marginaux dont je parle - aucune ligne de sortie pour l'heure où les horloges avancent, deux lignes pour l'heure où elles ont reculé (et cette heure s'est produite deux fois). Vous pouvez également jouer avec de mauvaises valeurs; si vous passez à 20140309 02:30 heure de l'Est, par exemple, ça ne marchera pas trop bien.
Je n'ai peut-être pas toutes les hypothèses sur le fonctionnement de vos rapports, vous devrez donc peut-être faire quelques ajustements. Mais je pense que cela couvre les bases.
la source
Pouvez-vous effectuer la transformation dans un proc stocké ou une vue paramétrée au lieu d'une couche de présentation? Une autre option consiste à créer un cube et à avoir les calculs dans le cube.
Explication des commentaires:
la source