如何调查BULK INSERT语句的性能?


12

我主要是使用Entity Framework ORM的.NET开发人员。但是,因为我不想失败使用ORM,所以我试图了解数据层(数据库)中发生的情况。基本上,在开发过程中,我启动了探查器,并检查了查询中代码的某些部分生成了什么。

如果我发现非常复杂的东西(即使不仔细编写,ORM甚至可以从相当简单的LINQ语句生成可怕的查询)和/或繁重的事情(持续时间,CPU,页面读取),请在SSMS中进行处理并检查其执行计划。

对于我的数据库知识水平,它工作正常。但是,BULK INSERT似乎是一种特殊的生物,因为它似乎不会产生SHOWPLAN

我将尝试说明一个非常简单的示例:

表定义

CREATE TABLE dbo.ImportingSystemFileLoadInfo
(
    ImportingSystemFileLoadInfoId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_ImportingSystemFileLoadInfo PRIMARY KEY CLUSTERED,
    EnvironmentId INT NOT NULL CONSTRAINT FK_ImportingSystemFileLoadInfo REFERENCES dbo.Environment,
    ImportingSystemId INT NOT NULL CONSTRAINT FK_ImportingSystemFileLoadInfo_ImportingSystem REFERENCES dbo.ImportingSystem,
    FileName NVARCHAR(64) NOT NULL,
FileImportTime DATETIME2 NOT NULL,
    CONSTRAINT UQ_ImportingSystemImportInfo_EnvXIs_TableName UNIQUE (EnvironmentId, ImportingSystemId, FileName, FileImportTime)
)

注意:表上未定义其他索引

批量插入 (我在探查器中捕获的内容,仅一批)

insert bulk [dbo].[ImportingSystemFileLoadInfo] ([EnvironmentId] Int, [ImportingSystemId] Int, [FileName] NVarChar(64) COLLATE Latin1_General_CI_AS, [FileImportTime] DateTime2(7))

指标

  • 插入了695个项目
  • CPU = 31
  • 读取= 4271
  • 写入= 24
  • 持续时间= 154
  • 表总数= 11500

对于我的应用程序,这没关系,尽管读取的内容似乎很大(我对SQL Server的内部知识了解甚少,所以我将其与8K页面大小和所拥有的少量记录信息进行了比较)

问题:如何调查是否可以优化此大容量插入?还是没有任何意义,因为它可以说是将大型数据从客户端应用程序推送到SQL Server的最快方法?

Answers:


14

据我所知,您可以以与优化常规插入非常相似的方式优化批量插入。通常,用于简单插入的查询计划不是非常有用,因此不必担心没有该计划。我将介绍几种优化插入物的方法,但是其中大多数可能不适用于您在问题中指定的插入物。但是,如果将来您需要加载大量数据,它们可能会有所帮助。

1.按集群键顺序插入数据

在将数据插入具有聚簇索引的表中之前,SQL Server通常会对其进行排序。对于某些表和应用程序,可以通过对平面文件中的数据进行排序并让SQL Server知道通过以下ORDER参数对数据进行排序来提高性能BULK INSERT

ORDER({列[ASC | DESC]} [,... n])

指定如何对数据文件中的数据进行排序。如果要导入的数据根据​​表上的聚簇索引(如果有)进行排序,则可以提高批量导入性能。

由于您使用IDENTITY列作为聚簇键,因此您不必为此担心。

2. TABLOCK尽可能使用

如果保证只有一个会话在表中插入数据,则可TABLOCK以为指定参数BULK INSERT。这可以减少锁争用,并且在某些情况下可以减少日志记录。但是,您将插入具有已经包含数据的聚簇索引的表,因此如果没有该跟踪标志610的跟踪标志610,您将不会得到最少的日志记录。

如果TABLOCK不可能,因为您无法更改代码,则不会失去所有希望。考虑使用sp_table_option

EXEC [sys].[sp_tableoption]
    @TableNamePattern = N'dbo.BulkLoadTable' ,
    @OptionName = 'table lock on bulk load' , 
    @OptionValue = 'ON'

另一选择是启用跟踪标志715

3.使用适当的批次大小

有时您可以通过更改批大小来调整插入。

ROWS_PER_BATCH = rows_per_batch

指示数据文件中数据的大概行数。

默认情况下,数据文件中的所有数据都作为单个事务发送到服务器,并且查询优化器不知道批处理中的行数。如果指定ROWS_PER_BATCH(值> 0),则服务器将使用该值来优化批量导入操作。为ROWS_PER_BATCH指定的值应与实际的行数大致相同。有关性能注意事项的信息,请参阅本主题后面的“备注”。

这是本文后面的引文:

如果单个批处理中要刷新的页面数超过内部阈值,则可能会发生对缓冲池的完整扫描,以识别提交批处理时要刷新的页面。此完整扫描可能会损害批量导入性能。当大型缓冲池与缓慢的I / O子系统结合使用时,可能会发生超过内部阈值的情况。为避免大型计算机上的缓冲区溢出,请不要使用TABLOCK提示(这将删除批量优化)或使用较小的批处理大小(保留批量优化)。

由于计算机各不相同,我们建议您在数据加载时测试各种批处理大小,以找出最适合您的数据。

就我个人而言,我只会在一个批处理中插入所有695行。但是,当插入大量数据时,调整批量大小可能会产生很大的不同。

4.确保您需要该IDENTITY

我对您的数据模型或需求一无所知,但也不会陷入IDENTITY向每个表添加列的陷阱。亚伦·伯特兰德(Aaron Bertrand)有一篇关于这种不良习惯的文章:在每张桌子上放一列IDENTITY。需要明确的是,我并不是说您应该IDENTITY从该表中删除该列。但是,如果您确定该IDENTITY列不是必需的,然后将其删除可以提高插入性能。

5.禁用索引或约束

如果与现有数据相比,正在将大量数据加载到表中,则在加载前禁用索引或约束并在加载后启用索引或约束可能会更快。对于大量数据,SQL Server一次全部建立索引通常比将数据加载到表中时效率更低。看来您将695行插入到具有11500行的表中,所以我不推荐这种技术。

6.考虑TF 610

跟踪标记610允许在某些其他方案中进行最少的日志记录。对于具有IDENTITY聚集键的表,只要恢复模型是简单记录或批量记录,您对任何新数据页的记录都将最少。我相信默认情况下该功能未启用,因为它可能会降低某些系统的性能。在启用此跟踪标志之前,您需要仔细测试。推荐的Microsoft参考似乎仍然是《数据加载性能指南》。

跟踪标志610下最小记录的I / O影响

当您提交最少记录的批量装入事务时,必须在提交完成之前将所有装入的页面刷新到磁盘。任何较早的检查点操作未捕获的已刷新页面都可以创建大量随机I / O。将此与完全记录的操作进行对比,该操作将在日志写入时创建顺序I / O,并且不需要在提交时将加载的页面刷新到磁盘。

如果您的负载方案是在不跨越检查点边界的btree上进行小型插入操作,并且您的I / O系统运行缓慢,则使用最少的日志记录实际上会降低插入速度。

据我所知,这与跟踪标志610没有任何关系,而与日志记录本身无关。我相信,有关ROWS_PER_BATCH调优的较早报价是采用相同的概念。

总之,您可能无法做很多调整BULK INSERT。我不会担心您在插入时观察到的读取计数。每当您插入数据时,SQL Server都会报告读取。考虑以下非常简单的内容INSERT

DROP TABLE IF EXISTS X_TABLE;

CREATE TABLE X_TABLE (
VAL VARCHAR(1000) NOT NULL
);

SET STATISTICS IO, TIME ON;

INSERT INTO X_TABLE WITH (TABLOCK)
SELECT REPLICATE('Z', 1000)
FROM dbo.GetNums(10000); -- generate 10000 rows

来自的输出SET STATISTICS IO, TIME ON

表“ X_TABLE”。扫描计数0,逻辑读取11428

我报告了11428次读取,但这不是可操作的信息。有时,可以通过最少的日志记录来减少报告的读取次数,但是当然不能将差异直接转换为性能提升。


12

我将开始回答这个问题,目的是在我建立技巧知识的基础上不断更新这个答案。希望其他人能遇到这个问题,并在此过程中帮助我提高自己的知识。

  1. 胆量检查:您的防火墙是否在进行有状态的深度数据包检查?在Internet上您不会找到太多相关信息,但是如果批量插入的速度比实际插入速度慢大约10倍,则可能是您的安全设备正在执行3-7级深度数据包检查并检查“通用SQL注入防护” ”。

  2. 测量您计划批量插入的数据大小(以字节为单位)。并检查是否要存储任何LOB数据,因为这是单独的页面获取和写入操作。

    您应该采用这种方式的几个原因:

    一种。在AWS中,Elastic Block Storage IOPS分解为字节,而不是行。

    1. 有关什么是EBS IOPS单元的说明,请参阅Linux实例上的Amazon EBS卷性能»I / O特性和监视
    2. 具体来说,通用SSD(gp2)卷具有“ I / O信用和突发性能”概念,对于繁重的ETL处理,通常会耗尽突发余额信用。突发持续时间以字节为单位,而不是SQL Server行:)

    b。尽管大多数库或白皮书都是基于行数进行测试的,但实际上这是可以写入的页面数,为了计算该数量,您需要知道每行多少字节以及页面大小(通常为8KB) ,但请务必仔细检查是否从其他人那里继承了系统。)

    SELECT *
    FROM 
    sys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID(N'YourTable'), NULL, NULL, 'DETAILED')

    请注意avg_record_size_in_bytes和page_count。

    C。正如Paul White在https://sqlperformance.com/2019/05/sql-performance/minimal-logging-insert-select-heap中所述,“要启用最小化日志记录INSERT...SELECT,SQL Server必须期望总大小超过250行至少一个范围(8页)。”

  3. 如果您有带有检查约束或唯一约束的索引,请使用SET STATISTICS IO ONSET STATISTICS TIME ON(或SQL Server Profiler或SQL Server Extended Events)捕获信息,例如您的大容量插入是否具有任何读取操作。读取操作归因于SQL Server数据库引擎确保完整性约束通过。

  4. 尝试创建一个测试数据库,其中PRIMARYFILEGROUP安装在RAM驱动器上。这应该比SSD快一点,但也消除了有关RAID控制器是否可能增加开销的任何问题。在2018年不应该这样做,但是通过创建像这样的多个差分基准,您可以大致了解硬件要增加多少开销。

  5. 还将源文件也放在RAM驱动器上。

    如果您正在从数据库服务器的FILEGROUP所在的同一驱动器读取源文件,则将源文件放在RAM驱动器上将排除所有争用问题。

  6. 验证是否已使用64KB扩展区格式化了硬盘驱动器。

  7. 使用UserBenchmark.com并对您的SSD进行基准测试。这将:

    1. 向其他性能狂热者添加更多有关设备预期性能的知识
    2. 帮助您确定驱动器的性能是否与完全相同的驱动器相比落后
    3. 帮助您确定驱动器的性能是否低于同类的其他驱动器(SSD,HDD等)
  8. 如果要通过Entity Framework Extensions从C#调用“ INSERT BULK”,请确保先“热身” JIT,然后“丢弃”前几个结果。

  9. 尝试为您的程序创建性能计数器。使用.NET,您可以使用Benchmark.NET,它将自动分析一系列基本指标。然后,您可以与开源社区共享您的探查器尝试,并查看运行不同硬件的人是否报告相同的指标(即从我之前关于使用UserBenchmark.com进行比较的角度来看)。

  10. 尝试使用命名管道并将其作为localhost运行。

  11. 如果您的目标是SQL Server并使用.NET Core,请考虑将Linux与SQL Server Std Edition结合使用-即使对于严重的硬件,每小时成本也不到一美元。在具有不同操作系统的相同硬件上尝试相同代码的主要优点是,查看操作系统内核的TCP / IP堆栈是否引起了问题。

  12. 使用Glen Barry的SQL Server诊断查询来测量存储数据库表FILEGROUP的驱动器的驱动器延迟。

    一种。确保在测试之前和测试之后进行测量。“测试之前”仅告诉您是否具有可怕的IO特性作为基准。

    b。为了测量“测试期间”,您确实需要使用PerfMon性能计数器。

    为什么?因为大多数数据库服务器都使用某种网络附加存储(NAS)。在云中,在AWS中,弹性块存储就是这样。您可能会受到EBS卷/ NAS解决方案的IOPS的束缚。

  13. 使用一些工具来衡量等待统计信息。 Red Gate SQL Monitor,SolarWinds数据库性能分析器,甚至Glen Barry的SQL Server诊断查询,或Paul Randal的Wait Statistics查询

    一种。最常见的等待类型可能是Memory / CPU,WRITELOG,PAGEIOLATCH_EX和ASYNC_NETWORK_IO

    b。如果您正在运行可用性组,则可能会导致其他等待类型。

  14. 测量禁用的多个同时执行的INSERT BULK命令的效果TABLOCK(TABLOCK可能会强制对INSERT BULK命令进行序列化)。您的瓶颈可能正在等待INSERT BULK完成。您应该尝试将数据库服务器的物理数据模型可以处理的任务最多排队。

  15. 考虑对表进行分区。作为一个特定的示例:如果您的数据库表是仅追加的,则Andrew Novick建议创建一个“ TODAY” FILEGROUP并划分为至少两个文件组,即TODAY和BEFORE_TODAY。这样,如果您的INSERT BULK数据只是今天的数据,则可以在CreatedOn字段上进行过滤,以强制所有插入均打到单个插入FILEGROUP,从而减少使用时的阻塞TABLOCK。Microsoft白皮书:使用SQL Server 2008的分区表和索引策略中更详细地介绍了此技术。

  16. 如果您使用的是列存储索引,请关闭TABLOCK并在102,400行“批量大小”中加载数据。然后,您可以将所有并行数据直接直接加载到列存储行组中。这个建议(以及合理的记载)来自微软的Columnstore索引-数据加载指南

    批量加载具有以下内置的性能优化:

    并行加载:您可以具有多个并发的批量加载(bcp或批量插入),每个都加载一个单独的数据文件。与将行存储批量加载到SQL Server中不同,您无需指定,TABLOCK因为每个批量导入线程都将数据排他地锁定到一个单独的行组(压缩或增量行组)中,并且专门排入数据。使用TABLOCK将在表上强制使用排他锁,并且您将无法并行导入数据。

    最小记录:批量加载对直接进入压缩行组的数据的日志记录最少。转到增量行组的所有数据均已完全记录。这包括任何小于102,400行的批量。但是,批量加载的目标是使大多数数据绕过增量行组。

    锁定优化:当加载到压缩的行组中时,将获得行组上的X锁定。但是,当批量加载到增量行组中时,在行组处会获得X锁,但SQL Server仍会锁定PAGE / EXTENT锁,因为X行组锁不是锁层次结构的一部分。

  17. 从SQL Server 2016开始,不再需要启用跟踪标志610才能最小化登录到索引表中。引用微软工程师Parikshit Savjani(重点是我):

    SQL Server 2016的设计目标之一是直接提高引擎的性能和可伸缩性,以使其运行更快,而无需客户使用任何旋钮或跟踪标志。作为这些改进的一部分,SQL Server引擎代码中的一项增强功能是打开批量加载上下文(也称为快速插入或快速加载上下文),并且在对具有简单或批量记录的恢复模型。如果您不熟悉最小日志记录,我强烈建议您阅读Sunil Agrawal的这篇博客文章,其中他解释了SQL Server中最小日志记录的工作原理。为了使散装插入件的最少记录,它仍然需要满足此处记录的前提条件。

    作为SQL Server 2016中这些增强功能的一部分,您不再需要启用跟踪标志610才能以最小方式登录到索引表中并且它与其他一些跟踪标志(1118、1117、1236、8048)结合在一起,成为历史的一部分。在SQL Server 2016中,当大容量加载操作导致要分配新页面时,如果满足了前面讨论的最小日志记录的所有其他先决条件,则最小填充地记录了顺序填充该新页面的所有行。插入现有页面(不分配新页面)以维持索引顺序的行以及加载过程中由于页面拆分而移动的行仍然被完全记录。对于索引(默认情况下为ON),将ALLOW_PAGE_LOCKS设置为ON也很重要,因为在分配期间获取页面锁时,最小的日志记录操作才能起作用,从而仅记录页面或扩展区分配。

  18. 如果您在C#或EntityFramework.Extensions(在后台使用SqlBulkCopy)中使用SqlBulkCopy,请检查您的构建配置。您是否在发布模式下运行测试?目标体系结构是否设置为任何CPU / x64 / x86?

  19. 考虑使用sp_who2查看INSERT BULK事务是否被挂起。它可能被暂停,因为它被另一个spid阻止了。考虑阅读如何最小化SQL Server阻塞。您也可以使用Adam Machanic的sp_WhoIsActive,但是sp_who2将为您提供所需的基本信息。

  20. 您可能只是磁盘I / O错误。如果您进行大容量插入,并且磁盘利用率未达到100%,并且停留在2%左右,则您的固件可能很坏,或者I / O设备有缺陷。(这是我的一个同事发生的。)使用[SSD UserBenchmark]与其他人进行比较,以了解其硬件性能,尤其是如果您可以在本地开发机器上复制慢的情况。(我将此列在列表的最后,因为由于IP风险,大多数公司不允许开发人员在其本地计算机上运行数据库。)

  21. 如果表使用压缩,则可以尝试运行多个会话,在每个会话中,首先使用现有事务,然后在SqlBulkCopy命令之前运行该事务

    更改服务器配置设置过程的亲和力CPU = AUTO;

  22. 对于连续加载,首先在Microsoft白皮书“ 使用SQL Server 2008进行分区的表和索引策略”中概述了一系列想法:

    连续加载

    在OLTP方案中,新数据可能会连续输入。如果用户也在查询最新分区,则连续插入数据可能导致阻塞:用户查询可能会阻塞插入,并且类似地,插入可能会阻塞用户查询。

    可以通过使用快照隔离(尤其是READ COMMITTED SNAPSHOT隔离级别)来减少对加载表或分区的争用。在READ COMMITTED SNAPSHOT隔离状态下,插入表不会在tempdb版本存储中引起活动,因此tempdb开销对于插入来说是最小的,但是在同一分区上的用户查询将不会使用共享锁。

    在其他情况下,当数据以高速率连续插入到分区表中时,您仍然可以在临时表中短时间暂存该数据,然后将该数据重复插入到最新的分区中,直到出现窗口为止。当前分区通过,然后将数据插入下一个分区。例如,假设您有两个登台表,每个登台表交替接收价值30秒的数据:一个表用于上半分钟,第二个表用于下半分钟。插入存储过程确定当前插入在哪一分钟的一半,然后将其插入到第一个登台表中。30秒后,插入过程将确定它必须插入第二个登台表中。然后,另一个存储过程将数据从第一个登台表加载到表的最新分区中,然后截断第一个登台表。再过30秒后,同一存储过程将从第二个存储过程中插入数据,并将其放入当前分区,然后截断第二个临时表。

  23. Microsoft CAT团队的“数据加载性能指南”

  24. 确保您的统计信息是最新的。如果可以在每次建立索引之后使用FULLSCAN。

  25. 使用SQLIO进行SAN性能调优,还请确保是否使用机械磁盘来对齐磁盘分区。请参阅Microsoft的磁盘分区对齐最佳实践

  26. COLUMNSTORE INSERT/ UPDATE表现


2

读取可能是插入过程中检查的唯一&FK约束-如果您可以在插入过程中禁用/删除它们,然后再启用/重新创建它们,则可能会提高速度。您需要测试与保持活动状态相比,这是否会使整体运行速度变慢。如果其他进程正在同时向同一表写入数据,那么这也不是一个好主意。- 加雷斯·里昂

根据“问与答”,外键在批量插入后变得不可信,FK约束在BULK INSERT没有CHECK_CONSTRAINTS选择的情况下变为不可信(我的情况是我以不可信约束结束)。尚不清楚,但检查它们并使其仍然不受信任是没有意义的。但是,仍然会检查PK和UNIQUE(请参阅BULK INSERT(Transact-SQL))。- 阿列克谢

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.