【问题标题】:Best SQL DB design for temp storage of millions of records用于临时存储数百万条记录的最佳 SQL DB 设计
【发布时间】:2014-03-27 16:11:50
【问题描述】:

我有一个数据库表,它以每台/秒/设备约 4 条记录的速度收集记录。这张桌子很快就变大了。当一个设备完成其任务时,另一个进程将遍历所有记录,执行一些操作,将它们组合成 5 分钟的块,压缩它们并存储它们以供以后使用。然后它会删除该表中该设备的所有记录。

目前有几台设备的近 100 万条记录。我可以很好地循环它们以执行处理,它出现了,但是当我尝试删除它们时,我超时了。有没有办法更快地删除这些记录?也许通过暂时关闭对象跟踪?使用一些锁定提示?当每个设备开始其任务时简单地为每个设备创建一个单独的表,然后在数据处理完成后将其删除,这种设计会更好吗?超时设置为 10 分钟。如果可能的话,我真的很想在 10 分钟内完成这个过程。

CREATE TABLE [dbo].[case_waveform_data] (
[case_id]                INT              NOT NULL,
[channel_index]          INT              NOT NULL,
[seconds_between_points] REAL             NOT NULL,
[last_time_stamp]        DATETIME         NOT NULL,
[value_array]            VARBINARY (8000) NULL,
[first_time_stamp]       DATETIME         NULL
);

CREATE CLUSTERED INDEX [ClusteredIndex-caseis-channelindex] ON [dbo].    [case_waveform_data]
(
[case_id] ASC,
[channel_index] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [NonClusteredIndex-all-fields] ON [dbo].[case_waveform_data]
(
[case_id] ASC,
[channel_index] ASC,
[last_time_stamp] ASC
)
INCLUDE (   [seconds_between_points],
[value_array]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON     PRIMARY]

SQL Server 2008+ 标准是数据库平台

2014 年 3 月 31 日更新:

我已经开始走上一条似乎有问题的道路。这真的有那么糟糕吗? 我正在创建一个存储过程,它接受一个包含我要附加的数据的表值参数和一个包含设备唯一表名的 varchar 参数。此存储过程将检查表是否存在,如果不存在,则使用特定结构创建它。然后它将插入来自 TVP 的数据。我看到的问题是我必须在 SP 中使用动态 sql,因为似乎无法将表名作为变量传递给 CREATE 或 INSERT。另外,我读到的每一篇关于如何做到这一点的文章都说不要......

不幸的是,如果我有一个表以 4/秒/设备的频率获取所有插入,那么即使在 case_id 和 channel_index 上使用聚集索引,仅对特定 case_id 的表进行计数也需要 17 分钟.因此,尝试删除它们大约需要 25 - 30 分钟。这也会导致发生锁定,因此插入开始花费的时间越来越长,从而导致服务落后。这甚至在没有删除的情况下也会发生。 所描述的存储过程旨在将插入从 4/sec/device 减少到 1/sec/device,并且可以在完成后删除表,而不是单独删除每条记录。想法?

2014 年 3 月 31 日更新 2

我没有按照您的想法使用游标或任何循环。这是我用来循环记录的代码。但是,它以可接受的速度运行:

using (SqlConnection liveconn = new SqlConnection(LiveORDataManager.ConnectionString))
{
    using (SqlCommand command = liveconn.CreateCommand())
    {
        command.CommandText = channelQueryString;
        command.Parameters.AddWithValue("channelIndex", channel);
        command.CommandTimeout = 600;
        liveconn.Open();

        SqlDataReader reader = command.ExecuteReader();

        // Call Read before accessing data. 
        while (reader.Read())
        {
            var item = new
            {
                //case_id = reader.GetInt32(0),
                channel_index = reader.GetInt32(0),
                last_time_stamp = reader.GetDateTime(1),
                seconds_between_points = reader.GetFloat(2),
                value_array = (byte[])reader.GetSqlBinary(3)
            };
            // Perform processing on item
        }
    }
}

我用来删除的SQL很琐碎:

DELETE FROM case_waveform_data where case_id = @CaseId

这一行需要 25+ 分钟才能删除 100 万行

样本数据(value_array 被截断):

case_id channel_index   seconds_between_points  last_time_stamp value_array first_time_stamp
7823    0   0.002   2014-03-31 15:00:40.660 0x1F8B0800000000000400636060    NULL
7823    0   0.002   2014-03-31 15:00:41.673 0x1F8B08000000000004006360646060F80F04201A04F8418C3F4082DBFBA2F29E5 NULL
7823    0   0.002   2014-03-31 15:00:42.690 0x1F8B08000000000004006360646060F80F04201A04F8418C3F4082DBFB    NULL

【问题讨论】:

  • 根据 first_time_stamp 删除超过 x 分钟前的所有内容的计划作业怎么样?
  • 用位标志来标记要删除的记录,然后在系统安静时再做?
  • 企业版还是标准版?如果是 Enterprise,听起来像是分区切换的工作......
  • 如果可能的话,我不希望在夜间作业中一次删除所有数据。另外,我不知道每次安装的安静时间是什么时候。数据只能在案例完成后删除,因此我通过 case_id 将其删除。案例可以持续不到一个小时或长达 60 天。
  • "...循环遍历所有记录..."、"...分别删除每条记录..." 这些是暗示游标或循环而不是基于集合的处理的可怕描述。数百万行通常不应该是一个问题,您的动态分区方案可能不是最好的方法。我认为您应该发布插入/更新/删除的 SQL、此表上的任何键/约束/索引,以及可能的一些示例行。

标签: performance sql-server-2008


【解决方案1】:

当从表中删除大量数据时,SQL Server 会在下面标记要删除的数据,并且作为后台作业,SQL Server 会在页面空闲时实际删除它们。也不同于截断;删除是启用日志。

如果您有企业版,正如其他开发人员建议的那样,使用分区是可能的方法,但您有标准版。

选项:1“更长、乏味、主观的 100% 性能”

假设您保留单一表格方法。您可以添加一个新列“IsProcessed”以指示哪些记录已被处理。当您插入新数据时,它将具有默认值 0,因此使用此数据的其他进程现在也将使用此列过滤其查询。处理后,您将需要对表进行额外更新以将这些行标记为 IsProcessed=1。现在,您可以创建 SQL Server JOB 以删除 Isprocessed=1 的前 N ​​行,并在理想的时间段尽可能频繁地安排该作业。 “TOP N”,因为您必须通过尝试和错误找出最适合您的环境的数字。可能是 100、1000、10,000。根据我的经验,如果数字较小,效果最好。增加作业执行的频率。假设“从表中删除前 1000 名”需要 2 分钟。并且当该表未被使用时,您有 4 小时的清洁窗口过夜,您可以安排此作业每 5 分钟运行一次。 3分钟只是缓冲。因此 12 exec/hour 和 4 小时内每次执行 1000 行,您将从表中删除 48k 行。然后在周末你有更大的窗口,你将不得不赶上剩余的行。 在这种方法中,您可以看到很多来回的广告细节,但不确定这是否会持续很长时间以满足您未来的需求。突然,输入的数据量翻了一番,你的所有计算都将崩溃。这种方法的另一个缺点是消费者对数据的查询现在必须依赖于 IsProcessed Column 的值。在您的特定情况下,消费者总是读取设备的所有数据,因此索引表对您没有帮助,反而会损害插入过程的性能。 我确实亲身体验过这个解决方案,并在我们的一个客户环境中持续了 2 年。

选项:2“快速、高效、对我有意义,可能对您有用”

为设备创建一个表,正如您提到的,如果不存在表,则使用存储过程动态创建表。这是我最近的经验,我们有元数据驱动的 ETL,所有 ETL 目标对象和 API 都是在运行时根据用户配置创建的。是的,它是动态 SQL,但如果使用得当并且一旦测试了性能,它还不错。这种方法的缺点是在初始阶段进行调试,如果某些东西不起作用。但是在您的情况下,您知道表结构并且已解决,您无需处理表结构的日常变化。这就是为什么我认为这更适合您的情况。另一件事是现在您还必须确保正确配置 TempDB,因为使用 TVP 和 temp 表会大大增加 tempDB 的使用量,因此您初始化和增加分配给 tempdb 的空间,tempDB 所在的磁盘是要查看的两个主要内容。正如我在选项 1 中所说,由于数据的消费者过程总是使用 ALL DATA,我认为您不需要任何额外的索引。事实上,我也会在没有任何指标的情况下测试性能。这就像处理所有暂存数据。

查看此方法的示例代码。如果您对这种方法的 inding 或任何其他方面感到积极和怀疑,请告诉我们。

准备架构对象

    IF OBJECT_ID('pr_DeviceSpecificInsert','P') IS NOT NULL
        DROP PROCEDURE pr_DeviceSpecificInsert
    GO
    IF  EXISTS  (
            SELECT  TOP 1   *
            FROM    sys.table_types
            WHERE   name    = N'udt_DeviceSpecificData'
        )
        DROP TYPE   dbo.udt_DeviceSpecificData
    GO
    CREATE TYPE dbo.udt_DeviceSpecificData
    AS TABLE
    (   
        testDeviceData  sysname NULL
    )
    GO
    CREATE PROCEDURE pr_DeviceSpecificInsert
    (
        @DeviceData     dbo.udt_DeviceSpecificData READONLY
        ,@DeviceName    NVARCHAR(200)
    )
    AS
    BEGIN
    SET NOCOUNT ON
    BEGIN TRY
    BEGIN TRAN
            DECLARE @SQL        NVARCHAR(MAX)=N''
                    ,@ParaDef   NVARCHAR(1000)=N''
                    ,@TableName NVARCHAR(200)=ISNULL(@DeviceName,N'')


            --get the UDT data into temp table
            --because we can not use UDT/Table Variable in dynamic SQL
            SELECT * INTO #Temp_DeviceData FROM @DeviceData


            --Drop and Recreate the Table for Device.
            BEGIN
                SET @SQL ='                         
                                if object_id('''+@TableName+''',''u'') IS NOT NULL
                                    drop table dbo.'+@TableName+'
                                CREATE TABLE dbo.'+@TableName+'
                                (
                                    RowID               INT IDENTITY NOT NULL
                                    ,testDeviceData     sysname NULL

                                )
                            '
                PRINT @SQL
                EXECUTE sp_executesql @SQL
            END

            --Insert the UDT data in to actual table
            SET @SQL ='
                            Insert INTO '+@TableName+N' (testDeviceData)
                            Select testDeviceData From #Temp_DeviceData
                    '
            PRINT @SQL
            EXECUTE sp_executesql @SQL

    COMMIT TRAN
    END TRY
    BEGIN CATCH
        ROLLBACK TRAN
        SELECT ERROR_MESSAGE()  
    END CATCH
    SET NOCOUNT OFF
    END

执行示例代码

    DECLARE @DeviceData dbo.udt_DeviceSpecificData
    INSERT @DeviceData (testDeviceData)
                SELECT 'abc'
    UNION ALL   SELECT 'xyz'

    EXECUTE dbo.pr_DeviceSpecificInsert
        @DeviceData = @DeviceData, -- udt_DeviceSpecificData
        @DeviceName = N'tbl2' -- nvarchar(200)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-15
    • 2015-10-04
    相关资源
    最近更新 更多