使用 SQLBulkCopy - SQL Server 2016 中的表比 SQL Server 2014 中的表大得多

Posted

技术标签:

【中文标题】使用 SQLBulkCopy - SQL Server 2016 中的表比 SQL Server 2014 中的表大得多【英文标题】:Using SQLBulkCopy - Significantly larger tables in SQL Server 2016 than in SQL Server 2014 【发布时间】:2018-04-20 23:57:08 【问题描述】:

我有一个使用 SqlBulkCopy 将数据移动到一组表中的应用程序。最近有消息称,使用 SQL2016 的用户报告了他们的硬盘驱动器被非常大的数据库(不应该那么大)填充的问题。 SQL2014不会出现这个问题。经检查,运行 TableDataSizes.sql(附加脚本)显示 UnusedSpaceKB 中有大量空间。

我想知道 a) SQLServer 2016 中是否存在一些错误,或者我们对 SQLBulkCopy 的使用是否与新功能“冲突”。我注意到 SQLServer 2016 中的页面分配发生了一些变化。总的来说 - 是什么原因造成的?

复制步骤 注意 – 下面描述了我看到的一种情况,其中删除了非必要信息。我实际上并没有在数据库表中存储数千个时间戳(其他列已被删除)。

    在 SQL 中创建一个数据库(我的称为 TestDB)

    在该数据库中创建一个表(使用如下脚本)

    USE [TestDB]
    GO
    
    /****** Object:  Table [dbo].[2017_11_03_DM_AggregatedPressure_Data]    Script Date: 07/11/2017 10:30:36 ******/
    SET ANSI_NULLS ON
    GO
    
    SET QUOTED_IDENTIFIER ON
    GO
    
    CREATE TABLE [dbo].[TestTable](
        [TimeStamp] [datetime] NOT NULL
    ) ON [PRIMARY]
    
    GO
    

    在该表上创建索引(使用如下脚本)

    USE [TestDB]
    GO
    
    /****** Object:  Index [2017_11_03_DM_AggregatedPressure_Data_Index]    Script Date: 07/11/2017 10:32:44 ******/
    CREATE CLUSTERED INDEX [TestTable_Index] ON [dbo].[TestTable]
    (
       [TimeStamp] 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)
    GO
    

    使用下面提供的代码开始将记录运行到表中。 (这是 Windows 窗体背后的代码,它上面只有一个名为 btnGo 的按钮和一个名为 nupRecordsToInsert 的 numericUpDown。

    Public Class Form1
    
    Private conStr As String = "Integrated Security=true;Persist Security Info=true;Server=.;Database=TestDB;Pooling=True"
    Dim tableName As String = "TestTable"
    
    Private Sub btnGo_Click(sender As Object, e As EventArgs) Handles btnGo.Click
    
        Dim table as DataTable = GetData(nupRecordsToInsert.Value)
    
        Using conn As SqlConnection = New SqlConnection(conStr)
            conn.Open()
            Using sbc As SqlBulkCopy = New SqlBulkCopy(conStr, SqlBulkCopyOptions.UseInternalTransaction Or SqlBulkCopyOptions.KeepIdentity)
    
                sbc.DestinationTableName = "[" & tableName & "]"
                sbc.BatchSize = 1000
                sbc.WriteToServer(table)
    
            End Using
        End Using
    
        MessageBox.Show($"Records Inserted = nupRecordsToInsert.Value into Database - TestDB. Table - tableName")
    End Sub
    
    Private Function GetData(numOfRecordsNeeded As Integer) As DataTable
        Dim table As DataTable = New DataTable()
        table.Columns.Add("TimeStamp", GetType(DateTime))   
    
        Dim dtDateTimeToInsert as DateTime = DateTime.Now
    
        For index As Integer = 1 To numOfRecordsNeeded
            dtDateTimeToInsert = dtDateTimeToInsert.AddSeconds(2)
            table.Rows.Add(dtDateTimeToInsert) 
        Next
    
        Return table
    End Function
    

    结束类

    在某个时候,数据库表中的项目数大约为 500 条,这意味着需要将新记录写入新页面。在这一点上,有趣的事情发生在实际结果中。

实际结果 SQL2016 中的数据库非常大(这发生在第一页已填满并启动第二页之后)。

时可以更详细地看到这一点

    运行以下 SQL 以了解表大小。 您在数据库中运行的记录越多,您在 UnusedSpaceKB 列中看到的非常大的数字就越多。

    use [TestDB]
    
    SELECT 
       t.NAME AS TableName,
       s.Name AS SchemaName,
       p.rows AS RowCounts,
       SUM(a.total_pages) * 8 AS TotalSpaceKB, 
       SUM(a.used_pages) * 8 AS UsedSpaceKB, 
       (SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS UnusedSpaceKB
    FROM 
       sys.tables t
    INNER JOIN      
       sys.indexes i ON t.OBJECT_ID = i.object_id
    INNER JOIN 
       sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
    INNER JOIN 
       sys.allocation_units a ON p.partition_id = a.container_id
    LEFT OUTER JOIN 
       sys.schemas s ON t.schema_id = s.schema_id
    WHERE 
      t.NAME = 'TestTable'
      AND t.is_ms_shipped = 0
      AND i.OBJECT_ID > 255 
    GROUP BY 
      t.Name, s.Name, p.Rows
    ORDER BY 
      RowCounts desc
    

在 UnusedSpaceKB 中显示大量的输出

    运行以下查询显示已分配了许多页面,但仅使用了每个“8 组”中的第一个。这使得每 8 页中的最后 7 页未被使用,从而造成大量空间浪费。

     select * from sys.dm_db_database_page_allocations
     (DB_id() , object_id('[dbo].[TestTable]') , NULL , NULL , 'DETAILED')
    

下面显示了页面分配不连续运行的部分结果。

SQL2014中的数据库没有出现这个问题 1. 运行适当的查询(如上)时,我们在 UnusedSpaceKB 列中看不到大值。

    运行另一个查询(即查询 - dm_db_database_page_allocations)显示已分配了许多页面,但每个页面都按顺序使用。没有间隙 - 没有 7 个未使用页面的块。

预期结果 我希望 SQL2016 的行为类似于 SQL2014,并且不会创建非常大的表。特别是,我希望页面是连续分配的,并且分配中没有 7 个页面间隙。

如果有人对我为什么看到这种差异有任何想法,那将非常有帮助。

【问题讨论】:

您是否检查过两台服务器上的服务器 FillFactor 是否相同?您的 CREATE INDEX 没有明确指定它,因此使用服务器默认值。附:为什么不在 BulkCopy 之后创建索引?正如你现在所做的那样,你永远不会有最少的日志记录 您以几乎最低效率的方式使用大容量复制——您在表上有一个聚集索引,批量大小为 1000,并且使用的是行锁而不是表锁。您仍将获得流式数据,但操作本身将被完全记录。但是,这本身不应从 SQL Server 2014 更改。两种情况下的恢复模型是否相同?是否应用了任何自定义跟踪标志? (如跟踪标志 610,它启用对具有聚簇索引的表的批量插入的最小日志记录)? 数据库的自动增长设置是什么?移动了多少数据? too large 是什么意思?这个问题的步骤太模糊,无法重现任何问题 一个区段是 8 页。看起来每个页面分配都是从一个新范围完成的。 Likely related。正如已经建议的那样,尝试增加批量大小(如果不能,文章还提到 TF 692 作为一种解决方法)。 (另外,很高兴知道 SQL Server 2016 中不再需要 TF 610 来在聚簇索引上获得最少记录的批量插入。) 请注意,批量插入已经并且将始终针对 bulk 插入进行优化——对于足够小的批量,您可以考虑在事务中切换到常规插入,这不会慢得多。 【参考方案1】:

你需要use trace flag 692:

如果由于任何原因,您无法更改批处理大小,或者如果您没有看到使用默认最小日志记录行为的数据加载性能有所提高,您可以使用跟踪标志 692 (...) 在 SQL Server 2016 中禁用快速插入行为。我们预计在正常情况下,客户不需要此跟踪标志。

【讨论】:

以上是关于使用 SQLBulkCopy - SQL Server 2016 中的表比 SQL Server 2014 中的表大得多的主要内容,如果未能解决你的问题,请参考以下文章

使用 SqlBulkCopy 将 DataTable 中的列映射到 SQL 表

使用 SQLBulkCopy 插入/更新数据库

使用 IDatareader 和 SqlBulkCopy 将 Dictionary 元素作为行插入 SQL 表

需要有关在 SQL Server 上使用 SqlBulkCopy 挑战极限的建议

SQL Server 2008 R2 的可重试 SQLBulkCopy

批量插入 SqlBulkCopy的测试