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

Ale*_*xei 12 sql-server execution-plan sql-server-2014 bulk-insert

我主要是使用实体框架 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)
)
Run Code Online (Sandbox Code Playgroud)

注意:表上没有定义其他索引

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

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

指标

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

对于我的应用程序,没关系,虽然读取看起来相当大(我对 SQL Server 内部了解很少,所以我比较了 8K 页面大小和我拥有的小记录信息)

问题:我如何调查这个 BULK INSERT 是否可以优化?或者它没有任何意义,因为它可以说是将大数据从客户端应用程序推送到 SQL Server 的最快方法?

Joe*_*ish 16

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

1.按聚类键顺序插入数据

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

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

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

由于您使用IDENTITY列作为聚集键,因此您无需担心这一点。

2.TABLOCK尽可能使用

如果保证只有一个会话向表中插入数据,则可TABLOCK以为指定参数BULK INSERT。这可以减少锁争用,并且在某些情况下可以导致最少的日志记录。但是,您要插入到具有已包含数据的聚集索引的表中,因此在没有本答案后面提到的跟踪标志 610 的情况下,您将无法获得最少的日志记录。

如果TABLOCK不可能,因为您无法更改代码,因此并非所有希望都将破灭。考虑使用sp_table_option

EXEC [sys].[sp_tableoption]
    @TableNamePattern = N'dbo.BulkLoadTable' ,
    @OptionName = 'table lock on bulk load' , 
    @OptionValue = 'ON'
Run Code Online (Sandbox Code Playgroud)

另一种选择是启用跟踪标志 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 参考仍然是The Data Loading Performance Guide

跟踪标志 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
Run Code Online (Sandbox Code Playgroud)

输出SET STATISTICS IO, TIME ON

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

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


Joh*_*ski 12

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

  1. 肠道检查:您的防火墙是否在进行有状态的深度数据包检查?您在 Internet 上找不到太多关于此的信息,但是如果您的批量插入速度比应有的速度慢 10 倍左右,那么您可能有一个安全设备在执行 3-7 级深度数据包检查并检查“通用 SQL 注入防护” ”。

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

    您应该这样做的几个原因:

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

    1. 请参阅Linux 实例上的 Amazon EBS 卷性能 » I/O 特性和监控以了解 EBS IOPS 单位是什么
    2. 具体而言,通用 SSD (gp2) 卷具有“I/O 积分和突发性能”概念,并且繁重的 ETL 处理通常会耗尽突发余额积分。您的突发持续时间以字节为单位,而不是 SQL Server 行:)

    湾 虽然大多数库或白皮书基于行数进行测试,但真正重要的是可以写入的页面数,为了计算它,您需要知道每行有多少字节和您的页面大小(通常为 8KB ,但请务必仔细检查您是否从其他人那里继承了系统。)

    SELECT *
    FROM 
    sys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID(N'YourTable'), NULL, NULL, 'DETAILED')
    
    Run Code Online (Sandbox Code Playgroud)

    注意 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 扩展事件)来捕获诸如批量插入是否有任何读取操作之类的信息。读取操作是由于 SQL Server 数据库引擎确保通过完整性约束。

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

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

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

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

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

    1. 向其他性能爱好者添加更多关于设备性能期望的知识
    2. 帮助您确定您的驱动器的性能是否低于具有相同驱动器的同行
    3. 帮助您确定您的驱动器的性能是否低于同一类别中的其他驱动器(SSD、HDD 等)
  8. 如果您通过实体框架扩展从 C# 调用“INSERT BULK”,那么请确保首先“预热”JIT 并“丢弃”前几个结果。

  9. 尝试为您的程序创建性能计数器。使用 .NET,您可以使用benchmark.NET,它会自动分析一堆基本指标。然后,您可以与开源社区分享您的分析器尝试,看看运行不同硬件的人是否报告了相同的指标(即我之前关于使用 UserBenchmark.com 进行比较的观点)。

  10. 尝试使用命名管道并将其作为本地主机运行。

  11. 如果您的目标是 SQL Server 并使用 .NET Core,请考虑使用 SQL Server 标准版启动 Linux - 即使对于严重的硬件,每小时的成本也低于 1 美元。在不同操作系统的相同硬件上尝试相同代码的主要优点是查看操作系统内核的 TCP/IP 堆栈是否导致问题。

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

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

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

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

  13. 使用一些工具来衡量等待统计。 Red Gate SQL Monitor、SolarWinds Database Performance Analyzer,甚至 Glen Barry 的 SQL Server 诊断查询,或Paul Randal 的 Wait Statistics 查询

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

    湾 如果您正在运行可用性组,则可能会产生额外的等待类型。

  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 行批处理大小的数据。然后,您可以将所有数据直接并行加载到列存储行组中。这个建议(和有据可查的理性)来自微软的列存储索引 - 数据加载指南

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

    并行加载:您可以有多个并发批量加载(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 中,当大容量加载操作导致分配新页面时,如果满足前面讨论的最小日志记录的所有其他先决条件,则顺序填充该新页面的所有行都会被最小化记录。插入到现有页面(没有新页面分配)以维护索引顺序的行仍然被完全记录,加载过程中由于页面拆分而移动的行也是如此。为索引打开 ALLOW_PAGE_LOCKS(默认情况下为 ON)也很重要,以便在分配期间获取页面锁时进行最少的日志记录操作,从而仅记录页面或范围分配。

  18. 如果您在 C# 或 EntityFramework.Extensions(在幕后使用 SqlBulkCopy)中使用 SqlBulkCopy,请检查您的构建配置。您是否在发布模式下运行测试?目标架构是否设置为 Any 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 命令之前运行它:

    ALTER SERVER CONFIGURATION SET PROCESS AFFINITY 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表现