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

Rob*_*ert 7 c# sql-server asp.net-mvc stored-procedures sql-server-2012

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

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

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

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

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

Vla*_*nov 8

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

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

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

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

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

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

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

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

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
Run Code Online (Sandbox Code Playgroud)

按原样将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
Run Code Online (Sandbox Code Playgroud)

注意,在这里我将所有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;
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

SELECT @VarCount = COUNT(*)
FROM dbo.StatsToRecalc
WITH (HOLDLOCK)
Run Code Online (Sandbox Code Playgroud)

如果登台表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
)
Run Code Online (Sandbox Code Playgroud)

然后用这个CTE加入每小时表MERGE:

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
)
...
Run Code Online (Sandbox Code Playgroud)

为了帮助进行这个多阶段求和,我在原始,每小时和每日表中都有帮助列.例如,每小时表有一个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
...
Run Code Online (Sandbox Code Playgroud)

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

2015-01-01 00:00:00
2015-01-02 00:00:00
...
Run Code Online (Sandbox Code Playgroud)

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

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