我主要是使用实体框架 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 项
- 中央处理器 = 31
- 读取 = 4271
- 写入 = 24
- 持续时间 = 154
- 总表数 = 11500
对于我的应用程序,没关系,虽然读取看起来相当大(我对 SQL Server 内部知识知之甚少,所以我比较了 8K 页面大小和我拥有的小记录信息)
问题:如何调查此 BULK INSERT 是否可以优化?或者它没有任何意义,因为它可以说是将大数据从客户端应用程序推送到 SQL Server 的最快方式?
据我所知,您可以以与优化常规插入非常相似的方式优化批量插入。通常,简单插入的查询计划信息量不大,因此不必担心没有计划。我将介绍几种优化插入的方法,但其中大多数可能不适用于您在问题中指定的插入。但是,如果您将来需要加载大量数据,它们可能会有所帮助。
1.按聚类键顺序插入数据
SQL Server 通常会在将数据插入具有聚集索引的表之前对其进行排序。对于某些表和应用程序,您可以通过对平面文件中的数据进行排序并让 SQL Server 知道数据是通过以下
ORDER
参数排序的,从而提高性能BULK INSERT
:由于您使用
IDENTITY
列作为聚集键,因此您无需担心这一点。2.
TABLOCK
尽可能使用如果保证只有一个会话向表中插入数据,则可
TABLOCK
以为BULK INSERT
. 这可以减少锁争用,并在某些情况下导致最少的日志记录。但是,您要插入到具有已包含数据的聚集索引的表中,因此如果没有跟踪标志 610,您将不会获得最少的日志记录,这将在本答案后面提到。如果
TABLOCK
不可能,因为你不能改变代码,并不是所有的希望都失去了。考虑使用sp_table_option
:另一种选择是启用跟踪标志 715。
3. 使用适当的批量大小
有时您可以通过更改批量大小来调整插入。
以下是文章后面的引述:
就我个人而言,我只会在一个批次中插入所有 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 无关,而是与最少的日志记录本身有关。我相信早先关于
ROWS_PER_BATCH
调优的引述也涉及到了同样的概念。总之,您可能无法调整
BULK INSERT
. 我不会担心您在插入时观察到的读取计数。每当您插入数据时,SQL Server 都会报告读取。考虑以下非常简单的问题INSERT
:输出
SET STATISTICS IO, TIME ON
:我报告了 11428 次读取,但这不是可操作的信息。有时可以通过最少的日志记录来减少报告的读取次数,但当然不能将差异直接转化为性能提升。
我将开始回答这个问题,目的是在我建立技巧知识库时不断更新这个答案。希望其他人能遇到这一点,并帮助我在此过程中提高自己的知识。
直觉检查:您的防火墙是否在进行有状态的深度数据包检查?您在 Internet 上找不到太多关于此的内容,但如果您的批量插入速度比应有的速度慢 10 倍,那么您可能有一个安全设备进行 3-7 级深度数据包检查并检查“通用 SQL 注入预防” ”。
测量您计划批量插入的数据的大小,以字节为单位,每批。并检查您是否存储了任何 LOB 数据,因为这是一个单独的页面读取和写入操作。
您应该这样做的几个原因:
一个。在 AWS 中,弹性块存储 IOPS 被分解为字节,而不是行。
湾。虽然大多数库或白皮书根据行数进行测试,但实际上是可以写入的页数,为了计算这一点,您需要知道每行有多少字节以及您的页面大小(通常为 8KB ,但请务必仔细检查您是否从其他人那里继承了该系统。)
注意 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 页)。”如果您有任何带有检查约束或唯一约束的索引,请使用
SET STATISTICS IO ON
andSET STATISTICS TIME ON
(或 SQL Server Profiler 或 SQL Server 扩展事件)来捕获信息,例如您的批量插入是否有任何读取操作。读取操作是由于 SQL Server 数据库引擎确保完整性约束通过。尝试创建一个测试数据库,其中PRIMARY
FILEGROUP
安装在 RAM 驱动器上。这应该比 SSD 稍快,但也消除了关于您的 RAID 控制器是否会增加开销的任何问题。在 2018 年,它不应该,但是通过创建多个像这样的差异基线,您可以大致了解硬件增加了多少开销。还将源文件也放在 RAM 驱动器上。
如果您从数据库服务器的 FILEGROUP 所在的同一驱动器读取源文件,则将源文件放在 RAM 驱动器上将排除任何争用问题。
确认您已使用 64KB 扩展区格式化您的硬盘驱动器。
使用UserBenchmark.com对您的 SSD 进行基准测试。这将:
如果您通过实体框架扩展从 C# 调用“INSERT BULK”,请确保首先“预热”JIT 并“丢弃”前几个结果。
尝试为您的程序创建性能计数器。使用 .NET,您可以使用benchmark.NET,它会自动分析一堆基本指标。然后,您可以与开源社区分享您的分析器尝试,并查看运行不同硬件的人是否报告相同的指标(即从我之前关于使用 UserBenchmark.com 进行比较的观点)。
尝试使用命名管道并将其作为 localhost 运行。
如果您的目标是 SQL Server 并使用 .NET Core,请考虑使用 SQL Server Std Edition 启动 Linux - 即使对于严重的硬件,每小时的成本也不到一美元。使用不同操作系统尝试使用相同硬件的相同代码的主要优点是查看操作系统内核的 TCP/IP 堆栈是否导致问题。
使用 Glen Barry 的 SQL Server 诊断查询来测量存储数据库表的 FILEGROUP 的驱动器的驱动器延迟。
一个。确保在测试之前和测试之后进行测量。“在你的测试之前”只是告诉你你是否有可怕的 IO 特征作为基线。
湾。对于“在测试期间”进行测量,您确实需要使用 PerfMon 性能计数器。
为什么?因为大多数数据库服务器使用某种网络附加存储(NAS)。在云中,在 AWS 中,弹性块存储就是这样。您可能会受到 EBS 卷/NAS 解决方案的 IOPS 的限制。
使用一些工具来衡量等待统计。 Red Gate SQL Monitor、SolarWinds Database Performance Analyzer,甚至 Glen Barry 的 SQL Server 诊断查询,或Paul Randal 的 Wait Statistics 查询。
一个。最常见的等待类型可能是 Memory/CPU、WRITELOG、PAGEIOLATCH_EX 和ASYNC_NETWORK_IO。
湾。如果您正在运行可用性组,您可能会产生额外的等待类型。
在禁用的情况下测量多个同时
INSERT BULK
命令的效果TABLOCK
(TABLOCK 可能会强制序列化 INSERT BULK 命令)。您的瓶颈可能正在等待 aINSERT BULK
完成;您应该尝试将数据库服务器的物理数据模型可以处理的尽可能多的这些任务排队。考虑对表进行分区。举个特别的例子:如果你的数据库表是只追加的,Andrew Novick 建议创建一个“TODAY”
FILEGROUP
并分区到至少两个文件组,TODAY 和 BEFORE_TODAY。这样,如果您的INSERT BULK
数据只是今天的数据,您可以过滤 CreatedOn 字段以强制所有插入命中单个FILEGROUP
,从而减少使用时的阻塞TABLOCK
。此技术在 Microsoft 白皮书:使用 SQL Server 2008 的分区表和索引策略中进行了更详细的描述如果您使用的是列存储索引,请关闭
TABLOCK
并加载 102,400 行 Batch Size 中的数据。然后,您可以将所有数据并行加载到列存储行组中。这个建议(以及记录在案的合理性)来自 Microsoft 的Columnstore 索引 - 数据加载指南:从 SQL Server 2016 开始,不再需要启用跟踪标志 610 以最小化登录索引表。引用微软工程师 Parikshit Savjani (强调我的):
如果您在 C# 或 EntityFramework.Extensions 中使用 SqlBulkCopy(在后台使用 SqlBulkCopy),请检查您的构建配置。您是否在发布模式下运行测试?目标体系结构是否设置为任何 CPU/x64/x86?
考虑使用 sp_who2 查看 INSERT BULK 事务是否已挂起。它可能被挂起,因为它被另一个 spid 阻止。考虑阅读如何最小化 SQL Server 阻塞。您也可以使用 Adam Machanic 的 sp_WhoIsActive,但 sp_who2 将为您提供所需的基本信息。
您可能只是有错误的磁盘 I/O。如果您进行批量插入并且您的磁盘利用率没有达到 100%,并且停留在 2% 左右,那么您可能有错误的固件或有缺陷的 I/O 设备。(这发生在我的一位同事身上。)使用 [SSD UserBenchmark] 与其他人比较硬件性能,特别是如果您可以在本地开发机器上复制慢速。(我把它放在列表的最后,因为由于 IP 风险,大多数公司不允许开发人员在他们的本地机器上运行数据库。)
如果您的表使用压缩,您可以尝试运行多个会话,并在每个会话中,从使用现有事务开始并在 SqlBulkCopy 命令之前运行它:
ALTER SERVER CONFIGURATION SET PROCESS AFFINITY CPU=AUTO;
对于连续加载,一个想法流首先在 Microsoft 白皮书《使用 SQL Server 2008 的分区表和索引策略》中进行了概述:
Microsoft CAT 团队的数据加载性能指南
确保您的统计数据是最新的。如果可以在每个索引构建后使用 FULLSCAN。
使用 SQLIO 进行 SAN 性能调优,如果您使用的是机械磁盘,请确保您的磁盘分区是对齐的。请参阅 Microsoft 的磁盘分区对齐最佳实践。
COLUMNSTORE
INSERT
/UPDATE
性能读取可能是在插入期间检查的唯一和 FK 约束 - 如果您可以在插入期间禁用/删除它们并在之后启用/重新创建它们,则可能会提高速度。与保持它们处于活动状态相比,您需要测试这是否会使整体速度变慢。如果其他进程同时写入同一个表,这也可能不是一个好主意。-加雷斯·里昂
根据 Q & A Foreign keys become untrusted after bulk insert,FK 约束在
BULK INSERT
没有CHECK_CONSTRAINTS
选项后变得不受信任(我的情况是我以不受信任的约束结束)。目前尚不清楚,但是检查它们并仍然使它们不受信任是没有意义的。但是,仍将检查 PK 和 UNIQUE(请参阅BULK INSERT (Transact-SQL))。-阿列克谢