数据库中自动数据聚合的最佳方法

Best approach to automatic data aggregation in DB

我们目前正在开发一个 Web 应用程序来处理位于数据库 table 中的大量存档数据。 table 中的数据行由一个唯一的行 ID、两个标识一台机器和一个数据点的 ID、一个值和一个时间戳组成。每当值更改超过给定阈值时,每台机器都会将其数据发送到此 table。 table 通常包含数百万到数亿个条目。

出于可视化目的,我创建了一个存储过程,它采用识别机器和数据点所需的两个 ID,以及开始和结束日期时间。然后它将开始和结束之间的值聚合成可变长度的块(通常为 15 分钟、1 小时、7 天等)和 returns 给定时间间隔内每个块的平均值、最小值和最大值。

该方法有效,但需要花费大量时间,即使有大量的数据库优化和索引。所以在前端图表页面上显示所选范围和机器的数据大约需要 10 到 60 秒,我认为这太多了。

所以我开始考虑创建一个新的 table,其中包含每个 "chunk" 的每台机器的预聚合数据。为了实现这一点,必须为每台机器每隔 [chunksize] minutes/hours/days 自动调用聚合过程。然后可以很容易地从更精细的块创建更粗糙的块,等等。据我所知,这将大大加快整个过程。

问题是:实现周期聚合的最佳方式是什么?有没有办法让数据库自己完成这项工作?或者我是否必须在 ASP.NET MVC Web 应用程序中实施基于计时器的解决方案?后者将要求网络应用程序始终 运行,这可能不是最佳方式,因为它可能会因各种原因而关闭。另一种选择是负责此任务的独立应用程序或服务。还有其他我没有想到的方法吗?你会如何处理这个问题?

在我们的系统中,我们有一个 table 具有原始原始数据。此原始数据汇总为每小时、每天和每周的时间间隔(每个时间间隔的原始值的总和、最小值、最大值)。

我们将原始数据保存 30 天(4 周),每小时保存 43 天(6 周),每天保存 560 天(18 个月),每周保存 10 年。每天晚上这四个 table 都是 "cleaned" 并且删除超过阈值的数据。每小时 table 大约有 3000 万行,每天 1800 万行。一些 reports/charts 使用每小时数据,大多数使用每日数据。有时我们需要查看原始数据以详细调查问题。

我有一个用 C++ 编写的专用应用程序,它 运行 在服务器上 24/7 全天候运行,并从大约 200 台其他服务器收集原始数据并将其插入中央数据库。在应用程序内部,我定期(每 10 分钟)调用一个重新计算摘要的存储过程。如果用户想查看最新的数据,最终用户也可以随时 运行 此存储过程。通常 运行 大约需要 10 秒,因此最终用户通常会看到延迟的摘要。因此,从技术上讲,服务器上可能有一个计划作业,运行 每 10 分钟执行一次该过程。当我通过应用程序执行此操作时,我可以更好地控制收集数据的其他线程。本质上,我会在汇总数据时暂停尝试插入新数据。但是,仅使用独立的存储过程也可以达到相同的效果。

就我而言,我可以使摘要的重新计算变得相当高效。

  • 随着新数据在这 10 分钟内流入数据库 window,我将原始数据直接插入主数据库 table。原始数据点永远不会更新,它们只会被添加(插入)。所以,这一步简单高效。我使用带有 table 值参数的存储过程,并在一次调用中传递一大块新数据。因此,在一个 INSERT 语句中插入了很多行,这很好。

  • 摘要 table 使用第二个存储过程每 10 分钟用新数据更新一次。必须更新一些现有行,添加一些行。为了有效地做到这一点,我有一个单独的 "staging" table,其中包含机器 ID、每小时日期时间、每天日期时间、每周日期时间列。当我将原始数据插入主 table 时,我还将受影响的机器 ID 和受影响的时间间隔插入到此暂存 table.

所以,有两个主要的存储过程。应用程序使用多个线程循环遍历 200 个远程服务器,并在无限循环中从每个服务器下载新数据。一旦从某个远程服务器下载了一批新数据,就会调用第一个存储过程。这种情况经常发生。此过程按原样将一批原始数据插入原始 table 并将受影响的时间间隔列表插入 "staging" table.

说,原始数据的传入批次如下所示:

ID timestamp            raw_value
1  2015-01-01 23:54:45  123
1  2015-01-01 23:57:12  456
1  2015-01-02 00:03:23  789
2  2015-01-02 02:05:21  909

4 行按原样插入主 table(ID、时间戳、值)。

3行被插入到staging中table(通常有很多具有同一小时时间戳的值,所以有很多原始行,但staging中很少table):

ID hourlytimestamp     dailytimestamp      weeklytimestamp
1  2015-01-01 23:00:00 2015-01-01 00:00:00 2014-12-29 00:00:00
1  2015-01-02 00:00:00 2015-01-02 00:00:00 2014-12-29 00:00:00
2  2015-01-02 00:00:00 2015-01-02 00:00:00 2014-12-29 00:00:00

请注意,我在这里 collate/condense/merge 将所有 ID 和时间戳放入唯一集合中,而此阶段 table 根本没有值,它仅包含受影响的 ID 和时间间隔(StatsToRecalc是这个staging table,@ParamRows是有一批带有新数据的行的存储过程的参数):

DECLARE @VarStart datetime = '20000103'; -- it is Monday
INSERT INTO dbo.StatsToRecalc
    (ID
    ,PeriodBeginLocalDateTimeHour
    ,PeriodBeginLocalDateTimeDay
    ,PeriodBeginLocalDateTimeWeek)
SELECT DISTINCT
    TT.[ID],
    -- Truncate time to 1 hour.
    DATEADD(hour, DATEDIFF(hour, @VarStart, TT.PlaybackStartedLocalDateTime), @VarStart),
    -- Truncate time to 1 day.
    DATEADD(day, DATEDIFF(day, @VarStart, TT.PlaybackStartedLocalDateTime), @VarStart),
    -- Truncate time to 1 week.
    DATEADD(day, ROUND(DATEDIFF(day, @VarStart, TT.PlaybackStartedLocalDateTime) / 7, 0, 1) * 7, @VarStart)
FROM @ParamRows AS TT;

然后从 @ParamRows.

简单 INSERT 到原始 table

因此,有许多 INSERTS 进入原始和暂存 tables 从许多线程使用此过程 10 分钟。

每 10 分钟调用一个重新计算摘要的第二个过程。

它做的第一件事是启动事务并锁定暂存 table 直到事务结束:

SELECT @VarCount = COUNT(*)
FROM dbo.StatsToRecalc
WITH (HOLDLOCK)

如果暂存 table StatsToRecalc 不为空,我们需要做一些事情。由于这个 table 被锁定,所有工作线程都不会干扰,并且会等到重新计算完成后再添加更多数据。

通过使用此暂存 table 我可以快速确定我需要重新计算哪些 ID 的小时、天和周。实际的汇总计算是在 MERGE 语句中完成的,它一次性处理所有受影响的 ID 和间隔。我运行三个MERGEs将原始数据汇总成每小时汇总,然后每小时汇总成每日,然后每天汇总成每周。然后 staging table 被清空(每 10 分钟一次),因此它永远不会变得太大。

每个 MERGE 首先列出自上次重新计算以来受到影响的 ID 和时间戳(例如,从每小时更新 table):

WITH
CTE_Changed (ID, PeriodBeginLocalDateTimeDay)
AS
(
    SELECT
        dbo.StatsToRecalc.ID
        , dbo.StatsToRecalc.PeriodBeginLocalDateTimeDay
    FROM 
        dbo.StatsToRecalc
    GROUP BY
        dbo.StatsToRecalc.ID
        ,dbo.StatsToRecalc.PeriodBeginLocalDateTimeDay
)

然后在 MERGE 中每小时 table 加入此 CTE:

MERGE INTO dbo.StatsDay AS Dest
USING 
(
    SELECT
        ...                 
    FROM 
        dbo.StatsHour
        INNER JOIN CTE_Changed ON 
            CTE_Changed.ID = dbo.StatsHour.ID AND
            CTE_Changed.PeriodBeginLocalDateTimeDay = dbo.StatsHour.PeriodBeginLocalDateTimeDay
)
...

为了帮助进行这种多阶段汇总,我在原始、每小时和每天 table 中提供了辅助列。例如,每小时 table 有一列 PeriodBeginLocalDateTimeHour,其中包含如下值:

2015-01-01 22:00:00
2015-01-01 23:00:00
2015-01-02 00:00:00
2015-01-02 01:00:00
...

,即一个小时的界限。同时还有第二列包含这些时间戳 "truncated" 到日期边界:PeriodBeginLocalDateTimeDay,其中包含如下值:

2015-01-01 00:00:00
2015-01-02 00:00:00
...

,即一天的界限。第二列仅在我将小时数汇总为天数时使用 - 我不必即时计算日期时间戳,而是使用持久的索引值。

我应该补充一点,在我的情况下,如果那个专用的 C++ 应用程序关闭一段时间是可以的。就是说数据会延迟10分钟以上,但不会丢失任何东西。