Design do Data Warehouse para geração de relatórios em relação a dados por vários fusos horários

10

Estamos tentando otimizar um design de data warehouse que ofereça suporte a relatórios de dados por muitos fusos horários. Por exemplo, podemos ter um relatório de um mês de atividade (milhões de linhas) que precisa mostrar a atividade agrupada por hora do dia. E é claro que a hora do dia deve ser a hora "local" para o fuso horário especificado.

Tivemos um design que funcionou bem quando apenas suportamos o UTC e uma hora local. O design padrão das dimensões Data e Hora para UTC e hora local, IDs nas tabelas de fatos. No entanto, essa abordagem não parece escalável se precisarmos suportar relatórios para mais de 100 fusos horários.

Nossas tabelas de fatos ficariam muito amplas. Além disso, teríamos que resolver o problema de sintaxe no SQL para especificar quais IDs de data e hora usar para agrupar em qualquer execução do relatório. Talvez uma declaração CASE muito grande?

Vi algumas sugestões para obter todos os dados pelo intervalo de horário UTC que você está cobrindo e depois retorná-lo à camada de apresentação para converter em local e agregado, mas testes limitados com o SSRS sugerem que será extremamente lento.

Também consultei alguns livros sobre o assunto, e todos parecem dizer que apenas têm UTC e convertem em exibição ou têm UTC e um local. Gostaria de receber quaisquer pensamentos e sugestões.

Nota: Esta pergunta é semelhante a: Manipulação de fusos horários no data mart / warehouse , mas não posso comentar sobre essa pergunta, então senti que isso merecia sua própria pergunta.

Atualização: selecionei a resposta de Aaron depois que ele fez algumas atualizações significativas e publicou exemplos de código e diagramas. Meus comentários anteriores sobre a resposta dele não farão mais muito sentido, pois se referiram à edição original da resposta. Vou tentar voltar e atualizar isso novamente, se necessário

Peter M
fonte
No contexto da minha resposta (e das atualizações que irei postar posteriormente), até que ponto os seus dados voltam? Um relatório mensal mostrará 28 a 31 conjuntos de blocos de 24 horas? Será sempre "um mês civil" ou será realmente algum intervalo? O que deve mostrar quando uma das datas é uma data de retorno / reversão de primavera no horário de verão para o fuso horário escolhido? Além disso, qual é exatamente a entrada para o relatório? Você converte a hora local do usuário para UTC automaticamente, com base na localidade atual, eles têm preferências, selecionam manualmente ou deduzem de alguma outra maneira ou desejam que a consulta descubra?
Aaron Bertrand
Para responder às suas perguntas: Os dados podem voltar até 2 anos. Temos alguns relatórios que mostram apenas um conjunto de blocos de 24 horas e outros que possuem um bloco de 24 horas por dia no período do relatório. O período pode realmente ser o que o usuário desejar. O usuário seleciona a começar e data final (e vezes) e, em seguida, seleciona o fuso horário que eles querem de uma lista suspensa
Peter M
possível duplicação de fusos horários
Jon of All Trades

Respostas:

18

Resolvi isso com uma tabela de calendário muito simples - cada ano tem uma linha por fuso horário suportado , com o deslocamento padrão e o horário de início / fim do horário de verão do DST e seu deslocamento (se esse fuso horário suportar). Em seguida, uma função embutida, vinculada ao esquema e com valor de tabela, que leva o tempo de origem (no UTC, é claro) e adiciona / subtrai o deslocamento.

Obviamente, isso nunca terá um desempenho extremamente bom se você estiver reportando uma grande parte dos dados; o particionamento pode parecer útil, mas você ainda terá casos em que as últimas horas em um ano ou as primeiras horas no próximo ano pertencem a um ano diferente quando convertidas em um fuso horário específico - para que você nunca possa obter uma partição verdadeira isolamento, exceto quando o intervalo de relatórios não incluir 31 de dezembro ou 1º de janeiro.

Existem alguns casos extremos estranhos que você precisa considerar:

  • 2014-11-02 05:30 UTC e 2014-11-02 06:30 UTC, ambos convertem para 01:30 no fuso horário do leste, por exemplo (um pela primeira vez 01:30 foi atingido localmente e depois um pela segunda vez, quando os relógios voltaram das 2:00 às 1:00 e mais meia hora se passou). Portanto, você precisa decidir como lidar com essa hora de geração de relatórios - de acordo com a UTC, você deve ver o dobro do tráfego ou volume do que estiver medindo quando essas duas horas forem mapeadas para uma única hora em um fuso horário que observe o horário de verão. Isso também pode jogar jogos divertidos com a sequência de eventos, já que algo que logicamente teve que acontecer depois que algo mais pudesse apareceracontecer antes que o tempo seja ajustado para uma única hora em vez de duas. Um exemplo extremo é uma exibição de página que aconteceu às 05:59 UTC e, em seguida, um clique que aconteceu às 06:00 UTC. No horário UTC, eles aconteciam com um minuto de diferença, mas, quando convertidos para o horário do leste, a exibição acontecia às 1:59 e o clique ocorria uma hora antes.

  • 2014-03-09 02:30 nunca acontece nos EUA. Isso ocorre porque às 02:00 rolamos os relógios para as 03:00. É provável que você queira gerar um erro se o usuário digitar esse horário e solicitar que você o converta para UTC ou crie seu formulário para que os usuários não possam escolher esse horário.

Mesmo com esses casos extremos em mente, ainda acho que você tem a abordagem correta: armazene os dados no UTC. Muito mais fácil mapear dados para outros fusos horários do UTC do que de algum fuso horário para outro fuso horário, especialmente quando fusos horários diferentes iniciam / encerram o horário de verão em datas diferentes e até o mesmo fuso horário pode alternar usando regras diferentes em anos diferentes ( por exemplo, os EUA mudaram as regras há seis anos).

Você desejará usar uma tabela de calendário para tudo isso, não alguma CASE expressão gigantesca (não declaração ). Acabei de escrever uma série de três partes para o MSSQLTips.com sobre isso; Eu acho que a terceira parte será a mais útil para você:

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/


Um verdadeiro exemplo ao vivo, enquanto isso

Digamos que você tenha uma tabela de fatos muito simples. O único fato que me interessa nesse caso é a hora do evento, mas adicionarei um GUID sem sentido apenas para tornar a tabela suficientemente ampla para se preocupar. Novamente, para ser explícito, a tabela de fatos armazena eventos apenas na hora UTC e na hora UTC. Eu mesmo coloquei o sufixo na coluna _UTCpara que não haja confusão.

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

Agora, vamos carregar nossa tabela de fatos com 10.000.000 de linhas - representando a cada 3 segundos (1.200 linhas por hora) de 30/12/2013 à meia-noite UTC até um pouco depois das 05:00 UTC em 12/12/2014. Isso garante que os dados ultrapassem o limite de um ano, bem como o horário de verão para a frente e para trás em vários fusos horários. Isso parece realmente assustador, mas levou ~ 9 segundos no meu sistema. A tabela deve acabar tendo cerca de 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 apenas para mostrar como será uma consulta de pesquisa típica nessa tabela de linhas de 10MM, se eu executar esta consulta:

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

Eu recebo esse plano e ele retorna em 25 milissegundos *, fazendo 358 leituras, para retornar 72 totais por hora:

insira a descrição da imagem aqui

* Duração conforme medida pelo nosso SQL Sentry Plan Explorer gratuito , que descarta os resultados; portanto, isso não inclui o tempo de transferência de dados, renderização etc. da rede. Como um aviso adicional, trabalho para o SQL Sentry.

Obviamente, levará um pouco mais de tempo se eu aumentar meu alcance - um mês de dados leva 258ms, dois meses leva 500ms e assim por diante. O paralelismo pode entrar em ação:

insira a descrição da imagem aqui

É aqui que você começa a pensar em outras soluções melhores para atender às consultas de relatórios e isso não tem nada a ver com o fuso horário em que a saída será exibida. Eu não vou entrar nisso, só quero demonstrar que a conversão de fuso horário realmente não fará com que suas consultas de relatórios sejam muito mais difíceis, e elas já podem ser ruins se você estiver obtendo grandes intervalos que não são compatíveis com o adequado índices. Vou manter pequenos intervalos de datas para mostrar que a lógica está correta e deixar você se preocupar em garantir que suas consultas de relatórios com base em intervalos tenham um desempenho adequado, com ou sem conversões de fuso horário.

Ok, agora precisamos de tabelas para armazenar nossos fusos horários (com deslocamentos, em minutos, já que nem todo mundo fica horas fora do UTC) e o horário de verão altera as datas para cada ano suportado. Para simplificar, vou inserir apenas alguns fusos horários e um único ano para corresponder aos dados acima.

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

Incluiu alguns fusos horários para variedade, alguns com desvios de meia hora, outros que não observam o horário de verão. Observe que a Austrália, no hemisfério sul, observa o horário de verão durante o inverno, então seus relógios voltam em abril e avançam em outubro. (A tabela acima inverte os nomes, mas não tenho certeza de como tornar isso menos confuso para os fusos horários do hemisfério sul.)

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

Agora, uma tabela de calendário para saber quando as TZs mudam. Vou inserir apenas linhas de interesse (cada fuso horário acima e apenas as alterações de horário de verão para 2014). Para facilitar os cálculos, eu armazeno o momento no UTC em que um fuso horário muda e o mesmo momento no horário local. Para fusos horários que não observam o horário de verão, é padrão o ano todo e o horário de verão "começa" em 1º de janeiro.

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

Você pode definitivamente preencher isso com algoritmos (e a próxima série de dicas usa algumas técnicas inteligentes baseadas em conjuntos, se é que eu digo), em vez de fazer um loop, preencher manualmente, o que você tem. Para esta resposta, decidi preencher manualmente um ano nos cinco fusos horários, e não vou incomodar nenhum truque sofisticado.

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

Ok, então temos nossos dados de fatos e nossas tabelas de "dimensão" (eu me encolho quando digo isso), então qual é a lógica? Bem, presumo que os usuários selecionem seu fuso horário e insiram o período da consulta. Também assumirei que o período será de dias inteiros no fuso horário; sem dias parciais, não importa horas parciais. Então eles passarão em uma data de início, uma data de término e um TimeZoneID. A partir daí, usaremos uma função escalar para converter a data de início / término desse fuso horário em UTC, o que nos permitirá filtrar os dados com base no intervalo UTC. Depois de fazer isso e executar nossas agregações, podemos aplicar a conversão dos tempos agrupados de volta ao fuso horário de origem, antes de exibir para o usuário.

O UDF escalar:

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 a função com valor de tabela:

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 um procedimento que o utiliza ( edit : updated para lidar com o agrupamento de deslocamento de 30 minutos):

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

(Você pode tentar entrar em curto-circuito lá, ou um procedimento armazenado separado, caso o usuário deseje gerar relatórios no UTC - obviamente, a conversão de e para o UTC será um trabalho muito trabalhoso).

Exemplo de chamada:

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

Retorna em 41ms * e gera este plano:

insira a descrição da imagem aqui

* Novamente, com resultados descartados.

Por 2 meses, ele retorna em 507ms, e o plano é idêntico, exceto contas de linha:

insira a descrição da imagem aqui

Embora um pouco mais complexo e aumentando o tempo de execução, estou bastante confiante de que esse tipo de abordagem funcionará muito, muito melhor do que a abordagem da tabela de pontes. E este é um exemplo imediato para uma resposta dba.se; Tenho certeza de que minha lógica e eficiência podem ser melhoradas por pessoas muito mais inteligentes que eu.

Você pode ler os dados para ver os casos extremos dos quais falo - nenhuma linha de saída para a hora em que os relógios avançam, duas linhas para a hora em que eles revertem (e essa hora aconteceu duas vezes). Você também pode jogar com valores ruins; se você passar 20140309 02:30 no horário do leste, por exemplo, não vai funcionar muito bem.

Talvez eu não tenha todas as suposições corretas sobre como seus relatórios funcionarão, portanto, talvez você precise fazer alguns ajustes. Mas acho que isso cobre o básico.

Aaron Bertrand
fonte
0

Você pode fazer a transformação em um processo armazenado ou em uma visualização parametrizada em vez da camada de apresentação? Outra opção é criar um cubo e ter os cálculos em cubo.

Explicação dos comentários:

O OP teve problemas de desempenho com seus testes limitados, fazendo os cálculos na camada de apresentação. Minha sugestão é mover isso para o banco de dados. No sql, você pode fazer uma exibição parametrizada usando uma função com valor de tabela. Com base no fuso horário passado para esta função, os dados podem ser calculados e retornados da tabela UTC. Espero que isso esclareça minha resposta original.

KNI
fonte
Então, uma exibição que possui mais de 100 colunas adicionais em que cada linha tem o horário de origem no UTC traduzido para todos os mais de 100 fusos horários? Eu não posso nem começar a entender como essa visão seria escrita. Observe também que o SQL Server não tem "exibição parametrizada" ...
Aaron Bertrand
hmm .. então é isso que você está pensando. e não foi isso que eu quis dizer.
KNI
11
Então me faça pensar o contrário. A propósito, não fui a favor do voto negativo, apenas tentando incentivar uma maior clareza na sua resposta.
Aaron Bertrand
O op teve problemas de desempenho com seus testes limitados, fazendo os cálculos na camada de apresentação. Minha sugestão é mover isso para o banco de dados. No sql, você pode fazer uma exibição parametrizada usando uma função com valor de tabela. Com base no fuso horário passado para esta função, os dados podem ser calculados e retornados da tabela utc. Espero que isso esclareça minha resposta original.
KNI
Como isso funciona se os dados são agregados? Se um fuso horário tiver um deslocamento de 30 minutos, os dados cairão em um grupo diferente. Você não pode simplesmente alterar os rótulos em exibição na camada de apresentação.
Colin 't Hart