Progettazione del data warehouse per la creazione di report sui dati per molti fusi orari


10

Stiamo cercando di ottimizzare un progetto di data warehouse che supporterà la creazione di report sui dati per molti fusi orari. Ad esempio, potremmo avere un rapporto per un mese di attività (milioni di righe) che deve mostrare l'attività raggruppata per ora del giorno. E, naturalmente, quell'ora del giorno deve essere l'ora "locale" per il fuso orario specificato.

Avevamo un design che funzionava bene quando abbiamo appena supportato UTC e un'ora locale. Il design standard delle dimensioni di data e ora per UTC e ora locale, ID nelle tabelle dei fatti. Tuttavia, tale approccio non sembra ridimensionarsi se dobbiamo supportare i rapporti per oltre 100 fusi orari.

Le nostre tabelle dei fatti sarebbero molto ampie. Inoltre, dovremmo risolvere il problema di sintassi in SQL specificando quale ID di data e ora utilizzare per il raggruppamento in una determinata esecuzione del report. Forse una dichiarazione CASE molto grande?

Ho visto alcuni suggerimenti per ottenere tutti i dati dall'intervallo di tempo UTC che stai trattando, quindi restituirli al livello di presentazione per convertirli in locali e aggregarli lì, ma test limitati con SSRS suggeriscono che sarà estremamente lento.

Ho consultato anche alcuni libri sull'argomento, e tutti sembrano dire che hanno solo UTC e convertono in mostra o hanno UTC e un locale. Gradirei qualsiasi pensiero e suggerimento.

Nota: questa domanda è simile a: Gestione dei fusi orari nel data mart / magazzino , ma non posso commentare quella domanda, quindi ho ritenuto che questo meritasse una sua domanda.

Aggiornamento: ho selezionato la risposta di Aaron dopo aver apportato alcuni aggiornamenti significativi e pubblicato esempi di codice e diagrammi. I miei precedenti commenti sulla sua risposta non avranno più molto senso poiché si riferivano alla modifica originale della risposta. Cercherò di tornare indietro e aggiornarlo di nuovo se garantito


Nel contesto della mia risposta (e degli aggiornamenti che inserirò in un secondo momento), a che distanza vanno i tuoi dati? Un rapporto mensile mostrerà 28-31 serie di blocchi di 24 ore? Sarà sempre "un mese di calendario" o potrebbe essere davvero qualsiasi intervallo? Cosa dovrebbe mostrare quando una delle date è una data di andata / ritorno primaverile dell'ora legale per il fuso orario scelto? Inoltre, qual è esattamente l'input per il rapporto? Converti automaticamente l'ora locale dell'utente in UTC in base alla locale corrente, hanno preferenze, selezionano manualmente o deduci in qualche altro modo o vuoi che la query lo capisca?
Aaron Bertrand

Per rispondere alle tue domande: i dati potrebbero risalire fino a 2 anni. Abbiamo alcuni rapporti che mostrano solo un set di blocchi di 24 ore e altri rapporti che presentano un blocco di 24 ore al giorno nell'intervallo di date del rapporto. L'intervallo di date può davvero essere qualsiasi cosa l'utente desideri. L'utente seleziona la data di inizio e di fine (e gli orari) e quindi seleziona il fuso orario desiderato da un menu a discesa
Peter M,

Risposte:


18

Ho risolto questo problema avendo una tabella di calendario molto semplice: ogni anno ha una riga per fuso orario supportato , con offset standard e data / ora di inizio / ora legale dell'ora legale e relativo offset (se quel fuso orario lo supporta). Quindi una funzione inline, associata a schema, valutata a tabella che prende il tempo di origine (ovviamente in UTC) e aggiunge / sottrae l'offset.

Ovviamente ciò non funzionerà mai molto bene se stai segnalando una grande porzione di dati; il partizionamento potrebbe sembrare utile, ma avrai comunque casi in cui le ultime ore in un anno o le prime ore nel prossimo anno appartengono effettivamente a un anno diverso quando vengono convertite in un fuso orario specifico, quindi non puoi mai ottenere una vera partizione isolamento, tranne quando l'intervallo di rapporti non include il 31 dicembre o il 1 gennaio.

Ci sono un paio di casi strani che devi considerare:

  • 2014-11-02 05:30 UTC e 2014-11-02 06:30 UTC vengono entrambi convertiti in 01:30 AM nel fuso orario orientale, ad esempio (uno per la prima volta 01:30 è stato colpito localmente, quindi uno per la seconda volta quando gli orologi sono tornati indietro dalle 2:00 all'01: 00 e è trascorsa un'altra mezz'ora). Quindi è necessario decidere come gestire quell'ora di reportistica: in base a UTC, dovresti vedere il doppio del traffico o del volume di qualsiasi cosa stai misurando una volta che queste due ore vengono mappate su una singola ora in un fuso orario che osserva l'ora legale. Questo può anche giocare a giochi divertenti con il sequenziamento di eventi, poiché qualcosa che logicamente doveva succedere dopo che poteva apparire qualcos'altroprima che avvenga una volta che il tempo è regolato su una sola ora anziché su due. Un esempio estremo è una visualizzazione di pagina avvenuta alle 05:59 UTC, quindi un clic che si è verificato alle 06:00 UTC. Nel tempo UTC questi avvenivano a distanza di un minuto, ma quando convertiti in fuso orario orientale, la vista avveniva alle 1:59 e il clic avveniva un'ora prima.

  • 09-03-2014 02:30 non succede mai negli Stati Uniti. Questo perché alle 2:00 AM spostiamo gli orologi in avanti alle 3:00 AM. Quindi probabilmente vorrai generare un errore se l'utente inserisce un tempo simile e ti chiede di convertirlo in UTC o di progettare il tuo modulo in modo che gli utenti non possano scegliere un orario del genere.

Anche con questi casi limite in mente, penso ancora che tu abbia l'approccio giusto: archiviare i dati in UTC. Molto più facile mappare i dati su altri fusi orari da UTC che da alcuni fusi orari ad altri fusi orari, soprattutto quando fusi orari diversi iniziano / terminano l'ora legale in date diverse e anche lo stesso fuso orario può passare a regole diverse in anni diversi ( per esempio gli Stati Uniti hanno cambiato le regole circa 6 anni fa).

Ti consigliamo di utilizzare una tabella di calendario per tutto ciò, non CASE un'espressione gigantesca (non un'istruzione ). Ho appena scritto una serie in tre parti per MSSQLTips.com su questo; Penso che la terza parte sarà la più utile per te:

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 vero esempio dal vivo, nel frattempo

Diciamo che hai una tabella dei fatti molto semplice. L'unico fatto che mi interessa in questo caso è l'ora dell'evento, ma aggiungerò un GUID insignificante solo per rendere il tavolo abbastanza ampio da occuparsene. Ancora una volta, per essere espliciti, la tabella dei fatti memorizza gli eventi solo in ora UTC e ora UTC. Ho anche aggiunto il suffisso alla colonna, _UTCquindi non c'è confusione.

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

Ora cariciamo la nostra tabella dei fatti con 10.000.000 di righe, che rappresentano ogni 3 secondi (1.200 righe all'ora) dal 30-12-2013 a mezzanotte UTC fino a qualche ora dopo le 5:00 UTC del 2014-12-12. Ciò garantisce che i dati si trovino a cavallo di un confine annuale, nonché l'ora legale avanti e indietro per più fusi orari. Sembra davvero spaventoso, ma ci sono voluti ~ 9 secondi sul mio sistema. La tabella dovrebbe finire per essere circa 325 MB.

;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;

E solo per mostrare come apparirà una tipica query di ricerca su questa tabella di righe da 10 MM, se eseguo questa query:

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

Ricevo questo piano e ritorna in 25 millisecondi *, facendo 358 letture, per restituire i totali di 72 ore:

inserisci qui la descrizione dell'immagine

* Durata misurata dal nostro piano di esplorazione SQL Sentry gratuito , che elimina i risultati, quindi questo non include il tempo di trasferimento in rete dei dati, il rendering, ecc. Come disclaimer aggiuntivo, lavoro per SQL Sentry.

Ci vuole un po 'più di tempo, ovviamente, se ingrandisco troppo il mio intervallo: un mese di dati impiega 258 ms, due mesi impiega oltre 500 ms e così via. Il parallelismo può dare il via a:

inserisci qui la descrizione dell'immagine

È qui che inizi a pensare ad altre soluzioni migliori per soddisfare le query di reporting e non ha nulla a che fare con quale fuso orario verrà visualizzato l'output. Non mi occuperò di questo, voglio solo dimostrare che la conversione del fuso orario non farà davvero risucchiare molto di più le tue query sui rapporti e potrebbero già risucchiare se stai ottenendo ampi intervalli che non sono supportati da indici. Seguirò piccoli intervalli di date per dimostrare che la logica è corretta e ti preoccuperò di assicurarti che le tue query sui rapporti basate su intervallo funzionino adeguatamente, con o senza conversioni di fuso orario.

Bene, ora abbiamo bisogno di tabelle per memorizzare i nostri fusi orari (con offset, in minuti, dato che non tutti hanno nemmeno ore di pausa rispetto all'ora UTC) e le date di modifica dell'ora legale per ogni anno supportato. Per semplicità, inserirò solo alcuni fusi orari e un solo anno per abbinare i dati sopra.

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

Incluso alcuni fusi orari per varietà, alcuni con offset di mezz'ora, altri che non osservano l'ora legale. Si noti che l'Australia, nell'emisfero meridionale, osserva l'ora legale durante il nostro inverno, quindi i loro orologi risalgono ad aprile e in avanti ad ottobre. (La tabella sopra mostra i nomi, ma non sono sicuro di come rendere questo meno confuso per i fusi orari dell'emisfero meridionale.)

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

Ora, una tabella del calendario per sapere quando cambiano le TZ. Inserirò solo righe di interesse (ogni fuso orario sopra e solo le modifiche all'ora legale per il 2014). Per facilitare i calcoli avanti e indietro, memorizzo sia il momento in UTC in cui cambia un fuso orario, sia lo stesso momento nell'ora locale. Per i fusi orari che non osservano l'ora legale, è standard tutto l'anno e l'ora legale inizia il 1 ° gennaio.

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])
);

Puoi sicuramente popolarlo con algoritmi (e la prossima serie di suggerimenti usa alcune tecniche basate su set intelligenti, se lo dico io stesso), piuttosto che loop, popolare manualmente, cosa hai. Per questa risposta ho deciso di popolare manualmente solo un anno per i cinque fusi orari e non mi preoccuperò di trucchi fantasiosi.

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');

Va bene, quindi abbiamo i nostri dati sui fatti e le nostre tabelle "dimensionali" (io rabbrividisco quando lo dico), quindi qual è la logica? Bene, presumo che gli utenti selezionino il loro fuso orario e inseriscano l'intervallo di date per la query. Supporrò anche che l'intervallo di date sarà di giorni interi nel loro fuso orario; nessun giorno parziale, non importa ore parziali. Quindi passeranno una data di inizio, una data di fine e un TimeZoneID. Da lì useremo una funzione scalare per convertire la data di inizio / fine da quel fuso orario in UTC, che ci permetterà di filtrare i dati in base all'intervallo UTC. Una volta fatto ciò, ed eseguito le nostre aggregazioni su di esso, possiamo quindi applicare la conversione dei tempi raggruppati al fuso orario di origine, prima di mostrarli all'utente.

L'UDF scalare:

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

E la funzione con valori di tabella:

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])
);

E una procedura che lo utilizza ( modifica : aggiornato per gestire il raggruppamento degli offset di 30 minuti):

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

(Potresti provare a cortocircuitare lì, o una procedura memorizzata separata, nel caso in cui l'utente desideri segnalare in UTC - ovviamente la traduzione da e verso UTC sarà uno spreco di lavoro occupato.)

Chiamata di esempio:

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

Restituisce in 41 ms * e genera questo piano:

inserisci qui la descrizione dell'immagine

* Ancora una volta, con risultati scartati.

Per 2 mesi, restituisce 507 ms e il piano è identico a parte i conteggi delle righe:

inserisci qui la descrizione dell'immagine

Sebbene leggermente più complesso e aumentando un po 'il tempo di esecuzione, sono abbastanza fiducioso che questo tipo di approccio funzionerà molto, molto meglio dell'approccio con tabella a ponte. E questo è un esempio fuori dal comune per una risposta dba.se; Sono sicuro che la mia logica ed efficienza potrebbero essere migliorate da persone molto più intelligenti di me.

Puoi esaminare i dati per vedere i casi limite di cui parlo: nessuna riga di output per l'ora in cui gli orologi si spostano in avanti, due righe per l'ora in cui sono passati (e quell'ora è avvenuta due volte). Puoi anche giocare con valori cattivi; se passi in 20140309 02:30 ora orientale, ad esempio, non funzionerà troppo bene.

Potrei non avere tutte le ipotesi giuste su come funzionerà la tua segnalazione, quindi potresti dover apportare alcune modifiche. Ma penso che questo copre le basi.


0

Puoi eseguire la trasformazione in un proc memorizzato o in una vista parametrizzata anziché nel livello di presentazione? Un'altra opzione è quella di creare un cubo e avere i calcoli nel cubo.

Spiegazione dai commenti:

OP ha riscontrato problemi di prestazioni con i suoi test limitati eseguendo i calcoli nel livello di presentazione. Il mio suggerimento è di spostarlo nel database. In sql è possibile eseguire una vista con parametri utilizzando una funzione con valori di tabella. In base al fuso orario passato a questa funzione, i dati possono essere calcolati e restituiti dalla tabella UTC. Spero che questo chiarisca la mia risposta originale.


Quindi una vista che ha più di 100 colonne aggiuntive in cui ogni riga ha l'ora di origine in UTC tradotta in tutti i 100+ fusi orari? Non riesco nemmeno a capire come sarebbe stata scritta una simile visione. Nota anche che SQL Server non ha una "vista parametrizzata" ...
Aaron Bertrand

hmm .. quindi è quello che stai pensando. e non intendevo questo.
KNI

1
Quindi fammi pensare diversamente. A proposito, non sono stato il voto negativo, ho solo cercato di incoraggiare una maggiore chiarezza nella tua risposta.
Aaron Bertrand

op ha riscontrato problemi di prestazioni con i suoi test limitati eseguendo i calcoli nel livello di presentazione. Il mio consiglio è di spostarlo nel database. In sql è possibile eseguire una vista con parametri utilizzando una funzione con valori di tabella. In base al fuso orario che viene passato a questa funzione, i dati possono essere calcolati e restituiti dalla tabella utc. Spero che questo chiarisca la mia risposta originale.
KNI,

Come può funzionare se i dati sono aggregati? Se un fuso orario è offset di 30 minuti, i dati rientreranno in un gruppo diverso. Non puoi semplicemente cambiare le etichette in mostra nel livello di presentazione.
Colin 't Hart,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.