数据仓库设计,用于针对多个时区的数据进行报告


10

我们正在尝试优化数据仓库设计,以支持针对许多时区的数据进行报告。例如,我们可能有一个关于一个月活动的报告(数百万行),该报告需要显示按一天中的小时分组的活动。当然,一天中的那个小时必须是给定时区的“本地”小时。

当我们仅支持UTC和一个本地时间时,我们的设计效果很好。事实表上的UTC和本地时间的日期和时间维度的标准设计。但是,如果我们必须支持100多个时区的报告,则该方法似乎无法扩展。

我们的事实表将变得非常广泛。另外,我们还必须解决SQL中的语法问题,即指定在报告的任何给定运行中使用哪个日期和时间ID进行分组。也许是一个非常大的CASE语句?

我已经看到了一些建议,可以按您覆盖的UTC时间范围获取所有数据,然后将其返回到表示层以转换为本地并在那里进行汇总,但是使用SSRS进行的有限测试表明这将非常慢。

我也参考了一些有关该主题的书籍,它们似乎都说只有UTC并可以进行转换,或者只有UTC和一个本地语言。将不胜感激任何想法和建议。

注意:此问题类似于:在数据集市/仓库中处理时区,但是我无法对此问题发表评论,因此感到这是值得的。

更新:在 Aaron进行了一些重大更新并发布了示例代码和图表之后,我选择了Aaron的答案。我先前对他的答案的评论不再有意义,因为它们涉及答案的原始编辑。如果有必要,我会尝试再次更新此内容


根据我的回答(以及稍后将发布的更新),您的数据走了多远?每月报告会显示28-31套24小时的数据块吗?它将始终是“日历月”还是真的可以在任何范围内?当其中一个日期是所选时区的DST春季前进/后退日期时,应该显示什么?此外,报告的确切输入是什么?您是否根据用户当前的语言环境自动将其本地时间转换为UTC,是否具有首选项,是否手动选择,还是以其他方式推断,还是希望查询算出?
亚伦·伯特兰

回答您的问题:数据可以追溯到2年。我们有一些报告仅显示一组24小时数据块,而其他报告在报告日期范围内每天都有24小时数据块。日期范围实际上可以是用户想要的任何内容。用户选择开始和结束日期(和时间),然后从下拉列表中选择所需的时区
Peter M

Answers:


18

我已经通过一个非常简单的日历表解决了这一问题-每年每个受支持的时区都有一行,其中包含标准偏移量以及DST的开始日期时间/结束日期时间及其偏移量(如果该时区支持的话)。然后是一个内联的,受模式约束的,表值函数,它占用源时间(当然,以UTC为单位)并添加/减去偏移量。

如果您要针对大量数据进行报告,那么这显然将永远无法表现出色;分区似乎有帮助,但是当转换为特定时区时,您仍然会遇到这样的情况:一年中的最后几个小时或次年的前几个小时实际上属于不同的年份-因此您永远无法获得真正的分区隔离,除非您的报告范围不包括12月31日或1月1日。

您需要考虑几种奇怪的情况:

  • 例如,在美国东部时区,2014-11-02 05:30 UTC和2014-11-02 06:30 UTC都转换为01:30 AM(第一次在本地时区打出01:30,然后一个时钟第二次从2:00 AM退回1:00 AM,又过了半小时)。因此,您需要确定如何处理这一小时的报告-根据UTC,一旦将这两个小时映射到遵守DST的时区中的一个小时,就应该看到所测量内容的流量或流量增加一倍。这也可以按事件顺序玩有趣的游戏,因为在某些其他事情可能出现之后,某些事情在逻辑上必须发生一旦时间调整为一个小时而不是两个小时,就可以在此之前发生。一个极端的例子是,在UTC时间05:59发生的网页浏览,然后在UTC 06:00发生的点击。在UTC时间中,这些时间间隔是一分钟,但是转换为东部时间后,观看时间是在1:59 AM发生的,而点击发生在一个小时之前。

  • 2014-03-09 02:30在美国从未发生过。这是因为在2:00 AM,我们将时钟向前滚动到3:00 AM。如果用户输入这样的时间并要求您将其转换为UTC,或者设计您的表单以使用户无法选择这样的时间,那么很可能会引发错误。

即使考虑到这些极端情况,我仍然认为您有正确的方法:将数据存储在UTC中。将数据从UTC映射到其他时区比从某个时区映射到其他时区要容易得多,尤其是当不同时区在不同日期开始/结束夏令时时,甚至同一时区也可以使用不同年份的不同规则进行切换(例如美国在6年前左右更改了规则)。

您将要为所有这些使用日历表,而不是一些庞大的CASE 表达式(不是statement)。我刚刚为此写了一个由三部分组成的系列,用于MSSQLTips.com。我认为第三部分对您最有用:

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/


同时,一个真实的例子

假设您有一个非常简单的事实表。在这种情况下,我唯一关心的事实是事件时间,但是我将添加一个无意义的GUID,只是为了使表足够宽,可以关心它。同样,要明确地说,事实表仅以UTC时间和UTC时间存储事件。我什至为该列加了后缀,_UTC所以没有混乱。

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

现在,让我们为事实表加载10,000,000行-从2013-12-30在UTC午夜到2014-12-12凌晨5点之后的某个时间,每3秒(每小时1,200行)。这样可以确保数据跨越一年边界,以及多个时区的DST向前和向后。这看起来确实很吓人,但在我的系统上花费了大约9秒钟。表最终应为大约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;

如果要运行此查询,仅显示此10MM行表的典型搜索查询的外观:

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

我得到了这个计划,它以25毫秒*的速度返回,进行358次读取,以返回72小时的总计:

在此处输入图片说明

* 持续时间由我们的免费SQL Sentry Plan Explorer度量,该结果会丢弃结果,因此不包括数据,呈现等的网络传输时间。作为一个免责声明,我使用SQL Sentry。

显然,如果我将范围设置得过大,则需要花费更长的时间-一个月的数据需要258ms,两个月的时间超过500ms,依此类推。并行性可能会开始:

在此处输入图片说明

在这里,您开始考虑其他更好的解决方案来满足报表查询,并且与您的输出将显示在哪个时区无关。我不打算讨论这个问题,我只是想证明时区转换并不会真正使您的报告查询变得更加糟糕,如果您得到的范围较大,而正常情况下不支持的话,它们可能已经很糟糕了。索引。我将坚持使用较小的日期范围来表明该逻辑是正确的,并且让您担心确保基于范围的报表查询在执行时区转换或不执行时区转换时均能正常执行。

好的,现在我们需要使用表格来存储我们的时区(以分钟为单位的偏移量,因为并不是每个人都比UTC少几个小时)和每个受支持年份的DST更改日期。为简单起见,我将只输入几个时区和一年以匹配上面的数据。

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

包括几个变化的时区,一些时区偏移半小时,有些不遵守DST。需要注意的是澳大利亚在南半球的冬天我们在观察DST,所以它们的时钟走回来在四月和十月。(上表颠倒了名字,但我不确定如何使南半球时区的混乱程度降低。)

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

现在,一个日历表可以知道TZ何时更改。我将只插入感兴趣的行(上面的每个时区,仅2014年的夏令时更改)。为了方便前后计算,我将时区更改的时间存储在UTC中,并将本地时间存储在相同时间。对于不遵守DST的时区,整年都是标准时间,DST将于1月1日开始。

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

您绝对可以使用算法来填充(如果我自己这么说的话,下一个技巧系列将使用一些基于集合的巧妙技巧),而不是循环地手动填充您所拥有的。对于这个答案,我决定只为五个时区手动填充一年,并且我不会打扰任何花哨的技巧。

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

好的,所以我们有了事实数据和“维”表(当我这么说时我很畏惧),那么逻辑是什么?好吧,我想您将让用户选择他们的时区并输入查询的日期范围。我还将假设日期范围将是其各自时区中的整天;没有部分时间,没关系部分时间。因此,它们将传递开始日期,结束日期和TimeZoneID。从那里开始,我们将使用标量函数将该时区的开始/结束日期转换为UTC,这将使我们能够基于UTC范围过滤数据。完成此操作并对其进行汇总后,我们可以在向用户显示之前将分组时间的转换应用回源时区。

标量UDF:

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

和表值函数:

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

以及使用它的过程(编辑:已更新为处理30分钟的偏移量分组):

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

(如果用户想要使用UTC报告,您可能希望在其中短路,或者在单独的存储过程中尝试-显然,往返于UTC将会浪费很多忙碌的工作。)

样品通话:

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

以41ms *返回,并生成以下计划:

在此处输入图片说明

* 同样,结果被丢弃。

2个月后,它返回507毫秒,除行数外,该计划是相同的:

在此处输入图片说明

尽管稍微复杂一些,并增加了运行时间,但我相当有信心这种方法将比桥接表方法有效得多。这是dba.se答案的现成示例;我敢肯定,比我聪明得多的人可以提高我的逻辑和效率。

您可以细读数据以查看我所谈论的极端情况-时钟向前滚动的小时没有输出,向后滚动的小时没有输出两行(该时间发生了两次)。您也可以玩低估值的游戏。例如,如果您在美国东部时间20140309 02:30过去,那将不能很好地工作。

对于您的报告的工作方式,我可能没有所有正确的假设,因此您可能需要进行一些调整。但是我认为这涵盖了基础知识。


0

您可以在存储的proc或参数化视图中而不是表示层中进行转换吗?另一种选择是创建一个多维数据集并将计算包含在多维数据集中。

评论说明:

OP通过在表示层中进行计算,在有限的测试中遇到了性能问题。我的建议是将其移至数据库。在sql中,您可以使用表值函数执行参数化视图。根据传递给此函数的时区,可以计算数据并从UTC表返回。希望这可以澄清我的原始答案。


因此,具有100多个附加列的视图,其中每行将UTC中的源时间转换为所有100多个时区?我什至无法开始理解如何编写这样的视图。还要注意,SQL Server没有“参数化视图” ...
Aaron Bertrand

嗯..这就是您的想法。那不是我的意思。
KNI

1
所以让我另想一想。顺便说一句,我并不是拒绝投票,只是想鼓励您的答案更加清晰。
亚伦·伯特兰

op在表示层进行计算时遇到了性能问题,原因是测试受限。我的建议是将其移至数据库。在sql中,您可以使用表值函数执行参数化视图。根据传递给此函数的时区,可以计算数据并从utc表返回。希望这可以澄清我的原始答案。
KNI 2014年

如果汇总了数据,怎么办?如果时区偏移30分钟,则数据将落入另一个组中。您不能只更改表示层中显示的标签。
Colin't Hart 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.