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

时间:2015-05-18 06:33:14

标签: c# sql-server asp.net-mvc stored-procedures sql-server-2012

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

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

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

所以我开始考虑为每个" chunk"创建一个包含每台机器的预聚合数据的新表。为了实现这一点,必须每隔[chunksize]分钟/小时/天自动调用聚合过程。然后可以从更精细的块等容易地创建更粗糙的块。据我所知,这将极大地加速整个事物。

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

1 个答案:

答案 0 :(得分:7)

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

我们将原始数据保存30天(4周),每小时保留43天(6周),每天保存560天(18个月),每周保存10年。每天晚上这四张桌子都被清理干净了#34;并删除早于阈值的数据。每小时表有大约30M行,每天18M行。一些报告/图表使用每小时数据,大多数使用每日数据。有时我们需要查看原始数据以详细调查问题。

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

就我而言,我可以更有效地重新计算摘要。

  • 当新数据在此10分钟窗口期间流入数据库时​​,我将原始数据直接插入主表。原始数据点永远不会更新,只会添加(插入)它们。因此,这一步骤简单而有效。我使用带有表值参数的存储过程,并在一次调用中传递一大块新数据。因此,在一个INSERT语句中插入了许多行,这很好。

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

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

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

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行插入主表(ID,时间戳,值)。

将3行插入到临时表中(通常有很多值带有同一小时的时间戳,因此有很多原始行,但在临时表中很少):

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

注意,在这里我将所有ID和时间戳整理/压缩/合并到唯一集中,并且此登台表根本没有值,它只包含受影响的ID和时间间隔(StatsToRecalc是此临时表@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;

然后INSERT的原始表中有简单的@ParamRows

因此,在许多线程中使用此过程持续10分钟的原始表和临时表中有许多INSERTS

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

它首先要做的是启动一个事务并锁定登台表直到事务结束:

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

如果登台表StatsToRecalc不为空,我们需要做点什么。当该表被锁定时,所有工作线程都不会干扰,并且会等到重新计算完成后再添加更多数据。

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

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

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中使用此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
)
...

为了帮助这个多阶段求和,我在原始,每小时和每日表中都有帮助列。例如,每小时表有一列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
...

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

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

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

我应该补充一点,在我的情况下,如果专用的C ++应用程序暂停一段时间就可以了。它只是意味着数据将延迟超过10分钟,但不会丢失任何东西。