Conception de l'entrepôt de données pour générer des rapports sur les données pour de nombreux fuseaux horaires


10

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é


Dans le contexte de ma réponse (et des mises à jour que j'y posterai plus tard), jusqu'où remontent vos données? Un rapport mensuel montrera-t-il 28 à 31 ensembles de morceaux de 24 heures? Sera-ce toujours "un mois civil" ou pourrait-il vraiment s'agir d'une plage? Que doit-il afficher lorsque l'une des dates est une date de printemps / aval de l'heure d'été pour le fuseau horaire choisi? En outre, quelle est exactement l'entrée pour le rapport? Convertissez-vous automatiquement l'heure locale de l'utilisateur en UTC en fonction de ses paramètres régionaux actuels, avez-vous des préférences, sélectionnez-vous manuellement ou déduisez-vous d'une autre manière ou voulez-vous que la requête le comprenne?
Aaron Bertrand

Pour répondre à vos questions: Les données peuvent remonter jusqu'à 2 ans. Nous avons certains rapports qui montrent un seul ensemble de blocs de 24 heures et d'autres rapports qui ont un bloc de 24 heures par jour dans la plage de dates du rapport. La plage de dates peut être vraiment tout ce que l'utilisateur souhaite. L'utilisateur sélectionne la date (et les heures) de début et de fin, puis sélectionne le fuseau horaire de son choix dans une liste déroulante
Peter M

duplication possible des fuseaux horaires
Jon of All Trades

Réponses:


18

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 _UTCpour qu'il n'y ait pas de confusion.

CREATE TABLE dbo.Fact
(
  EventTime_UTC DATETIME NOT NULL,
  Filler UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID()
);
GO

CREATE CLUSTERED INDEX x ON dbo.Fact(EventTime_UTC);
GO

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.

;WITH x(c) AS 
(
  SELECT TOP (10000000) DATEADD(SECOND, 
    3*(ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1),
    '20131230')
  FROM sys.all_columns AS s1
  CROSS JOIN sys.all_columns AS s2
  ORDER BY s1.[object_id]
)
INSERT dbo.Fact WITH (TABLOCKX) (EventTime_UTC) 
  SELECT c FROM x;

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:

SELECT DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0),
  COUNT(*)
FROM dbo.Fact 
WHERE EventTime_UTC >= '20140308'
AND EventTime_UTC < '20140311'
GROUP BY DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0);

J'obtiens ce plan, et il revient en 25 millisecondes *, faisant 358 lectures, pour retourner 72 totaux horaires:

entrez la description de l'image ici

* 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:

entrez la description de l'image ici

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.

CREATE TABLE dbo.TimeZones
(
  TimeZoneID TINYINT    NOT NULL PRIMARY KEY,
  Name       VARCHAR(9) NOT NULL,
  Offset     SMALLINT   NOT NULL, -- minutes
  DSTName    VARCHAR(9) NOT NULL,
  DSTOffset  SMALLINT   NOT NULL  -- minutes
);

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.)

INSERT dbo.TimeZones VALUES
(1, 'UTC',     0, 'UTC',     0),
(2, 'GMT',     0, 'BST',    60), 
     -- London = UTC in winter, +1 in summer
(3, 'EST',  -300, 'EDT',  -240), 
     -- East coast US (-5 h in winter, -4 in summer)
(4, 'ACDT',  630, 'ACST',  570), 
     -- Adelaide (Australia) +10.5 h Oct - Apr, +9.5 Apr - Oct
(5, 'ACST',  570, 'ACST',  570); 
     -- Darwin (Australia) +9.5 h year round

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.

CREATE TABLE dbo.Calendar
(
  TimeZoneID    TINYINT NOT NULL FOREIGN KEY
                REFERENCES dbo.TimeZones(TimeZoneID),
  [Year]        SMALLDATETIME NOT NULL,
  UTCDSTStart   SMALLDATETIME NOT NULL,
  UTCDSTEnd     SMALLDATETIME NOT NULL,
  LocalDSTStart SMALLDATETIME NOT NULL,
  LocalDSTEnd   SMALLDATETIME NOT NULL,
  PRIMARY KEY (TimeZoneID, [Year])
);

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.

INSERT dbo.Calendar VALUES
(1, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00'),
(2, '20140101', '20140330 01:00','20141026 00:00','20140330 02:00','20141026 01:00'),
(3, '20140101', '20140309 07:00','20141102 06:00','20140309 03:00','20141102 01:00'),
(4, '20140101', '20140405 16:30','20141004 16:30','20140406 03:00','20141005 02:00'),
(5, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00');

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:

CREATE FUNCTION dbo.ConvertToUTC
(
  @Source   SMALLDATETIME,
  @SourceTZ TINYINT
)
RETURNS SMALLDATETIME
WITH SCHEMABINDING
AS
BEGIN
  RETURN 
  (
    SELECT DATEADD(MINUTE, -CASE 
        WHEN @Source >= src.LocalDSTStart 
         AND @Source < src.LocalDSTEnd THEN t.DSTOffset 
        WHEN @Source >= DATEADD(HOUR,-1,src.LocalDSTStart) 
         AND @Source < src.LocalDSTStart THEN NULL
        ELSE t.Offset END, @Source)
    FROM dbo.Calendar AS src
    INNER JOIN dbo.TimeZones AS t 
    ON src.TimeZoneID = t.TimeZoneID
    WHERE src.TimeZoneID = @SourceTZ 
      AND t.TimeZoneID = @SourceTZ
      AND DATEADD(MINUTE,t.Offset,@Source) >= src.[Year]
      AND DATEADD(MINUTE,t.Offset,@Source) < DATEADD(YEAR, 1, src.[Year])
  );
END
GO

Et la fonction table:

CREATE FUNCTION dbo.ConvertFromUTC
(
  @Source   SMALLDATETIME,
  @SourceTZ TINYINT
)
RETURNS TABLE
WITH SCHEMABINDING
AS
 RETURN 
 (
  SELECT 
     [Target] = DATEADD(MINUTE, CASE 
       WHEN @Source >= trg.UTCDSTStart 
        AND @Source < trg.UTCDSTEnd THEN tz.DSTOffset 
       ELSE tz.Offset END, @Source)
  FROM dbo.Calendar AS trg
  INNER JOIN dbo.TimeZones AS tz
  ON trg.TimeZoneID = tz.TimeZoneID
  WHERE trg.TimeZoneID = @SourceTZ 
  AND tz.TimeZoneID = @SourceTZ
  AND @Source >= trg.[Year] 
  AND @Source < DATEADD(YEAR, 1, trg.[Year])
);

Et une procédure qui l'utilise ( édition : mise à jour pour gérer le regroupement de décalages de 30 minutes):

CREATE PROCEDURE dbo.ReportOnDateRange
  @Start      SMALLDATETIME, -- whole dates only please! 
  @End        SMALLDATETIME, -- whole dates only please!
  @TimeZoneID TINYINT
AS 
BEGIN
  SET NOCOUNT ON;

  SELECT @Start = dbo.ConvertToUTC(@Start, @TimeZoneID),
         @End   = dbo.ConvertToUTC(@End,   @TimeZoneID);

  ;WITH x(t,c) AS
  (
    SELECT DATEDIFF(MINUTE, @Start, EventTime_UTC)/60, 
      COUNT(*) 
    FROM dbo.Fact 
    WHERE EventTime_UTC >= @Start
      AND EventTime_UTC <  DATEADD(DAY, 1, @End)
    GROUP BY DATEDIFF(MINUTE, @Start, EventTime_UTC)/60
  )
  SELECT 
    UTC = DATEADD(MINUTE, x.t*60, @Start), 
    [Local] = y.[Target], 
    [RowCount] = x.c 
  FROM x OUTER APPLY 
    dbo.ConvertFromUTC(DATEADD(MINUTE, x.t*60, @Start), @TimeZoneID) AS y
  ORDER BY UTC;
END
GO

(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:

EXEC dbo.ReportOnDateRange 
  @Start      = '20140308', 
  @End        = '20140311', 
  @TimeZoneID = 3;

Retourne en 41ms *, et génère ce plan:

entrez la description de l'image ici

* Encore une fois, avec des résultats rejetés.

Pendant 2 mois, il revient en 507ms, et le plan est identique à part les rowcounts:

entrez la description de l'image ici

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.


0

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:

OP a rencontré des problèmes de performances avec ses tests limités en effectuant les calculs dans la couche de présentation. Ma suggestion est de déplacer cela dans la base de données. Dans sql, vous pouvez faire une vue paramétrée à l'aide d'une fonction table. En fonction du fuseau horaire transmis à cette fonction, les données peuvent être calculées et renvoyées à partir de la table UTC. J'espère que cela clarifie ma réponse originale.


Donc, une vue qui a plus de 100 colonnes supplémentaires où chaque ligne a l'heure source en UTC traduite dans tous les 100+ fuseaux horaires? Je ne peux même pas commencer à comprendre comment une telle vue serait écrite. Notez également que SQL Server n'a pas de "vue paramétrée" ...
Aaron Bertrand

hmm .. c'est ce que vous pensez. et ce n'est pas ce que je voulais dire.
KNI

1
Alors fais-moi penser le contraire. Soit dit en passant, je n'étais pas en train de voter pour essayer d'encourager une meilleure clarté dans votre réponse.
Aaron Bertrand

op a rencontré des problèmes de performances avec ses tests limités en effectuant les calculs dans la couche de présentation. Ma suggestion est de déplacer cela vers la base de données. Dans sql, vous pouvez faire une vue paramétrée à l'aide d'une fonction table. En fonction du fuseau horaire transmis à cette fonction, les données peuvent être calculées et renvoyées à partir de la table utc. J'espère que cela clarifie ma réponse originale.
KNI

Comment cela peut-il fonctionner si les données sont agrégées? Si un fuseau horaire est décalé de 30 minutes, les données tomberont dans un groupe différent. Vous ne pouvez pas simplement modifier les étiquettes affichées dans la couche de présentation.
Colin 't Hart
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.