Diseño de Data Warehouse para informar contra datos de muchas zonas horarias

10

Estamos tratando de optimizar un diseño de depósito de datos que admitirá la presentación de informes contra datos para muchas zonas horarias. Por ejemplo, podríamos tener un informe de un mes de actividad (millones de filas) que necesita mostrar la actividad agrupada por hora del día. Y, por supuesto, esa hora del día tiene que ser la hora "local" para la zona horaria dada.

Teníamos un diseño que funcionaba bien cuando solo admitíamos UTC y una hora local. El diseño estándar de las dimensiones de fecha y hora para UTC y hora local, identificación en las tablas de hechos. Sin embargo, ese enfoque no parece escalar si tenemos que admitir informes para más de 100 zonas horarias.

Nuestras tablas de hechos serían muy amplias. Además, tendríamos que resolver el problema de sintaxis en SQL de especificar qué id. De fecha y hora usar para agrupar en cualquier ejecución del informe. Tal vez una declaración de CASO muy grande?

He visto algunas sugerencias para obtener todos los datos por el rango de tiempo UTC que está cubriendo, luego devolverlo a la capa de presentación para convertirlo a local y agregado allí, pero las pruebas limitadas con SSRS sugieren que será extremadamente lento.

También he consultado algunos libros sobre el tema, y ​​todos parecen decir que solo tienen UTC y convertir en exhibición o tienen UTC y uno local. Agradecería cualquier pensamiento y sugerencia.

Nota: Esta pregunta es similar a: Manejo de zonas horarias en data mart / warehouse , pero no puedo comentar sobre esa pregunta, por lo que sentí que merecía su propia pregunta.

Actualización: Seleccioné la respuesta de Aaron después de que realizó algunas actualizaciones significativas y publicó códigos de muestra y diagramas. Mis comentarios anteriores sobre su respuesta ya no tendrán mucho sentido, ya que se referían a la edición original de la respuesta. Intentaré volver y actualizar esto nuevamente si se justifica

Peter M
fuente
En contexto a mi respuesta (y las actualizaciones que publicaré más adelante), ¿qué tan atrás van sus datos? ¿Un informe mensual mostrará 28-31 conjuntos de fragmentos de 24 horas? ¿Será siempre "un mes calendario" o podría ser realmente cualquier rango? ¿Qué debería mostrar cuando una de las fechas es una fecha de avance / retroceso del horario de verano para la zona horaria elegida? Además, ¿cuál es exactamente la entrada para el informe? ¿Convierte automáticamente la hora local del usuario a UTC en función de su configuración regional actual, tiene preferencias, selecciona manualmente, deduce de alguna otra manera o desea que la consulta lo resuelva?
Aaron Bertrand
Para responder a sus preguntas: los datos podrían retroceder hasta 2 años. Tenemos algunos informes que muestran solo un conjunto de fragmentos de 24 horas y otros informes que tienen un fragmento de 24 horas por cada día en el rango de fechas del informe. El rango de fechas realmente puede ser cualquier cosa que el usuario quiera. El usuario selecciona la fecha de inicio y finalización (y las horas) y luego selecciona la zona horaria que desea de un menú desplegable
Peter M
posible duplicado de zonas horarias
Jon of All Trades

Respuestas:

18

He resuelto esto teniendo una tabla de calendario muy simple: cada año tiene una fila por zona horaria admitida , con el desplazamiento estándar y la fecha / hora de inicio / finalización del horario de verano y su desplazamiento (si esa zona horaria lo admite). Luego, una función en línea, vinculada al esquema y con valores de tabla que toma el tiempo de origen (en UTC, por supuesto) y suma / resta el desplazamiento.

Obviamente, esto nunca funcionará extremadamente bien si está informando sobre una gran parte de los datos; La partición puede parecer útil, pero aún tendrá casos en los que las últimas horas en un año o las primeras horas en el próximo año realmente pertenecen a un año diferente cuando se convierten a una zona horaria específica, por lo que nunca puede obtener una partición verdadera aislamiento, excepto cuando su rango de informes no incluye el 31 de diciembre o el 1 de enero.

Hay un par de casos extraños que debes considerar:

  • 2014-11-02 05:30 UTC y 2014-11-02 06:30 UTC ambos se convierten a 01:30 AM en la zona horaria del Este, por ejemplo (uno por primera vez 01:30 fue golpeado localmente, y luego uno por segunda vez cuando los relojes retrocedieron de las 2:00 a.m. a la 1:00 a.m., y transcurrió otra media hora). Por lo tanto, debe decidir cómo manejar esa hora de informes; de acuerdo con UTC, debería ver el doble del tráfico o el volumen de lo que esté midiendo una vez que esas dos horas se asignen a una sola hora en una zona horaria que observe el horario de verano. Esto también puede jugar juegos divertidos con secuencia de eventos, ya que algo que lógicamente tuvo que suceder después de que algo más pudiera aparecerocurrir antes de eso una vez que el tiempo se ajusta a una sola hora en lugar de dos. Un ejemplo extremo es una vista de página que ocurrió a las 05:59 UTC, luego un clic que ocurrió a las 06:00 UTC. En la hora UTC, esto sucedió con un minuto de diferencia, pero cuando se convirtió a la hora del Este, la vista ocurrió a la 1:59 a.m., y el clic ocurrió una hora antes.

  • 2014-03-09 02:30 nunca sucede en los Estados Unidos. Esto se debe a que a las 2:00 a.m., adelantamos los relojes a las 3:00 a.m. Por lo tanto, es probable que desee generar un error si el usuario ingresa ese tiempo y le pide que lo convierta a UTC, o que diseñe su formulario para que los usuarios no puedan elegir ese tiempo.

Incluso con esos casos límite en mente, sigo pensando que tiene el enfoque correcto: almacenar los datos en UTC. Es mucho más fácil asignar datos a otras zonas horarias desde UTC que desde alguna zona horaria a otra zona horaria, especialmente cuando diferentes zonas horarias comienzan / terminan el horario de verano en diferentes fechas, e incluso la misma zona horaria puede cambiar usando diferentes reglas en diferentes años ( por ejemplo, EE. UU. cambió las reglas hace aproximadamente 6 años).

Deberá usar una tabla de calendario para todo esto, no una CASE expresión gigantesca (no una declaración ). Acabo de escribir una serie de tres partes para MSSQLTips.com sobre esto; Creo que la tercera parte será la más útil para ti:

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 verdadero ejemplo en vivo, mientras tanto

Digamos que tiene una tabla de hechos muy simple. El único hecho que me importa en este caso es el tiempo del evento, pero agregaré un GUID sin sentido solo para hacer que la tabla sea lo suficientemente amplia como para preocuparse. Nuevamente, para ser explícitos, la tabla de hechos almacena eventos en tiempo UTC y solo en tiempo UTC. Incluso he agregado el sufijo a la columna _UTCpara que no haya confusión.

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

Ahora, carguemos nuestra tabla de hechos con 10,000,000 filas, que representan cada 3 segundos (1,200 filas por hora) desde 2013-12-30 a la medianoche UTC hasta algún momento después de las 5 AM UTC del 2014-12-12. Esto garantiza que los datos abarquen un límite de un año, así como el horario de verano hacia adelante y hacia atrás para múltiples zonas horarias. Esto parece realmente aterrador, pero tardó ~ 9 segundos en mi sistema. La tabla debería terminar siendo de unos 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;

Y solo para mostrar cómo se verá una consulta de búsqueda típica en esta tabla de filas de 10MM, si ejecuto 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);

Recibo este plan, y regresa en 25 milisegundos *, haciendo 358 lecturas, para devolver 72 totales por hora:

ingrese la descripción de la imagen aquí

* Duración medida por nuestro Explorador de planes de SQL Sentry gratuito , que descarta los resultados, por lo que esto no incluye el tiempo de transferencia de red de los datos, la representación, etc. Como descargo de responsabilidad adicional, trabajo para SQL Sentry.

Obviamente, tarda un poco más si hago que mi rango sea demasiado grande: un mes de datos tarda 258 ms, dos meses lleva más de 500 ms, y así sucesivamente. El paralelismo puede entrar en acción:

ingrese la descripción de la imagen aquí

Aquí es donde comienza a pensar en otras soluciones mejores para satisfacer las consultas de informes, y no tiene nada que ver con la zona horaria que mostrará su salida. No voy a entrar en eso, solo quiero demostrar que la conversión de zona horaria realmente no hará que sus consultas de informes absorban mucho más, y es posible que ya lo hagan si obtiene grandes rangos que no son compatibles con el adecuado índices Me limitaré a los pequeños intervalos de fechas para mostrar que la lógica es correcta y dejar que se preocupe por asegurarse de que sus consultas de informes basadas en el rango funcionen adecuadamente, con o sin conversiones de zona horaria.

Bien, ahora necesitamos tablas para almacenar nuestras zonas horarias (con compensaciones, en minutos, ya que no todos están incluso horas sin UTC) y las fechas de cambio de horario de verano para cada año admitido. Para simplificar, solo voy a ingresar unas pocas zonas horarias y un solo año para que coincida con los datos anteriores.

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

Incluyó algunas zonas horarias para la variedad, algunas con compensaciones de media hora, algunas que no observan el horario de verano. Tenga en cuenta que Australia, en el hemisferio sur, observa el horario de verano durante nuestro invierno, por lo que sus relojes retroceden en abril y avanzan en octubre. (La tabla anterior voltea los nombres, pero no estoy seguro de cómo hacer que esto sea menos confuso para las zonas horarias del hemisferio sur).

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

Ahora, una tabla de calendario para saber cuándo cambian las TZ. Solo voy a insertar filas de interés (cada zona horaria anterior, y solo los cambios de horario de verano para 2014). Para facilitar los cálculos de ida y vuelta, almaceno el momento en UTC donde cambia la zona horaria y el mismo momento en la hora local. Para las zonas horarias que no observan el horario de verano, es estándar durante todo el año, y el horario de verano "comienza" el 1 de enero.

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

Definitivamente, puede completar esto con algoritmos (y la próxima serie de consejos utiliza algunas técnicas inteligentes basadas en conjuntos, si lo digo yo mismo), en lugar de bucle, complete manualmente, ¿qué tiene? Para esta respuesta, decidí rellenar manualmente un año para las cinco zonas horarias, y no voy a molestarme con ningún truco elegante.

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

Bien, entonces tenemos nuestros datos de hechos y nuestras tablas de "dimensiones" (me estremezco cuando digo eso), entonces, ¿cuál es la lógica? Bueno, supongo que va a hacer que los usuarios seleccionen su zona horaria e ingresen el rango de fechas para la consulta. También supondré que el rango de fechas será días completos en su propia zona horaria; sin días parciales, no importa las horas parciales. Por lo tanto, pasarán una fecha de inicio, una fecha de finalización y un TimeZoneID. A partir de ahí, utilizaremos una función escalar para convertir la fecha de inicio / finalización de esa zona horaria a UTC, lo que nos permitirá filtrar los datos en función del rango UTC. Una vez que hayamos hecho eso, y hayamos realizado nuestras agregaciones en él, podemos aplicar la conversión de los tiempos agrupados nuevamente a la zona horaria de origen, antes de mostrar al usuario.

El 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

Y la función con valores de tabla:

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

Y un procedimiento que lo usa ( editar : actualizado para manejar la agrupación de desplazamiento 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

(Es posible que desee realizar un cortocircuito allí, o un procedimiento almacenado por separado, en el caso de que el usuario quiera informar en UTC; obviamente, la traducción hacia y desde UTC será un trabajo muy ocupado).

Llamada de muestra:

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

Devuelve en 41 ms * y genera este plan:

ingrese la descripción de la imagen aquí

* Nuevamente, con resultados descartados.

Durante 2 meses, regresa en 507 ms, y el plan es idéntico aparte de los recuentos de filas:

ingrese la descripción de la imagen aquí

Si bien es un poco más complejo y aumenta un poco el tiempo de ejecución, estoy bastante seguro de que este tipo de enfoque funcionará mucho, mucho mejor que el enfoque de la mesa de bridge. Y este es un ejemplo poco convencional para una respuesta dba.se; Estoy seguro de que mi lógica y eficiencia podrían ser mejoradas por personas mucho más inteligentes que yo.

Puede leer detenidamente los datos para ver los casos límite de los que hablo: no hay fila de salida para la hora en que los relojes avanzan, dos filas para la hora en que retrocedieron (y esa hora sucedió dos veces). También puedes jugar con malos valores; si pasa en 20140309 02:30 hora del este, por ejemplo, no va a funcionar demasiado bien.

Es posible que no tenga todas las suposiciones correctas sobre cómo funcionarán sus informes, por lo que es posible que deba hacer algunos ajustes. Pero creo que esto cubre lo básico.

Aaron Bertrand
fuente
0

¿Se puede hacer la transformación en un proceso almacenado o una vista parametrizada en lugar de una capa de presentación? Otra opción es crear un cubo y tener los cálculos en cubo.

Explicación de los comentarios:

OP se topó con problemas de rendimiento con sus pruebas limitadas al hacer los cálculos en la capa de presentación. Mi sugerencia es mover eso a la base de datos. En sql, puede hacer una vista parametrizada utilizando una función con valores de tabla. Según la zona horaria que se pasa a esta función, los datos se pueden calcular y devolver desde la tabla UTC. Espero que esto aclare mi respuesta original.

KNI
fuente
Entonces, ¿una vista que tiene más de 100 columnas adicionales donde cada fila tiene la hora de origen en UTC traducida a las más de 100 zonas horarias? Ni siquiera puedo comenzar a comprender cómo se escribiría tal punto de vista. También tenga en cuenta que SQL Server no tiene "vista parametrizada" ...
Aaron Bertrand
hmm ... así que eso es lo que estás pensando. Y eso no es lo que quise decir.
KNI
1
Así que hazme pensar lo contrario. Por cierto, no fui el voto negativo, solo intentaba alentar una mejor claridad en su respuesta.
Aaron Bertrand
op se topó con problemas de rendimiento con sus pruebas limitadas al hacer los cálculos en la capa de presentación. Mi sugerencia es mover eso a la base de datos. En sql, puede hacer una vista parametrizada utilizando una función con valores de tabla. Según la zona horaria que se pasa a esta función, los datos se pueden calcular y devolver desde la tabla utc. Espero que esto aclare mi respuesta original.
KNI
¿Cómo puede funcionar esto si los datos se agregan? Si una zona horaria tiene un desplazamiento de 30 minutos, los datos caerán en un grupo diferente. No puede simplemente cambiar las etiquetas en exhibición en la capa de presentación.
Colin 't Hart