bcp/BULK INSERT与表值参数的性能比较

86
我即将不得不使用SQL Server的BULK INSERT命令重写一些相当古老的代码,因为架构已经改变。我意识到也许我应该考虑改用一个带有TVP的存储过程,但我想知道它对性能可能会产生什么影响。
以下是一些背景信息,这些信息可能有助于解释我提出这个问题的原因:
- 数据实际上是通过Web服务传入的。Web服务将一个文本文件写入数据库服务器上的共享文件夹,然后执行BULK INSERT。这个过程最初是在SQL Server 2000上实现的,在当时,除了向服务器抛几百个INSERT语句之外,没有其他选择,而实际上,这个过程是一场性能灾难。 - 数据被批量插入到永久的暂存表中,然后合并到一个更大的表中(之后从暂存表中删除)。 - 要插入的数据量是“大”的,但不是“巨大”的—通常是几百行,偶尔在5-10k行左右。因此,我的直觉是,BULK INSERT是一个非记录操作,不会产生那么大的影响(但当然我不确定,所以提出这个问题)。 - 插入实际上是一个更大的管道式批处理的一部分,并需要连续多次发生;因此性能是至关重要的。
我想用TVP替换BULK INSERT的原因是:
- 通过NetBIOS写入文本文件可能已经耗费了一些时间,从架构角度来看非常糟糕。 - 我相信暂存表可以(而且应该)被消除。它存在的主要原因是插入的数据需要在插入同时用于另外几个更新,而尝试从巨大的生产表中进行更新比使用几乎为空的暂存表更为昂贵。使用TVP,参数基本上就是暂存表,我可以在主要插入之前/之后对其进行任何操作。 - 我可以基本上放弃重复检查、清理代码和与批量插入相关的所有开销。 - 如果服务器同时接收到这些事务的话,不需要担心暂存表或tempdb上的锁争用(我们尽量避免,但也会发生)。
我显然会在投入任何东西到生产环境之前进行性能测试,但我认为在花费所有时间之前问问周围的人是否对将TVP用于此目的有任何严厉的警告是一个好主意。那么,对于那些熟悉SQL Server 2008并尝试或至少调查过此功能的人来说,结论是什么?对于插入几百到几千行数据,且频率相当高的情况下,使用表值参数是否足够好?与批量插入相比,性能上是否有显著差异?
更新:现在只剩下92%的问号了!
(也就是测试结果)
这个最终结果经过了一个似乎有36个阶段的部署流程后已经投入生产。两种解决方案都经过了广泛的测试:
1.切换到存储过程并使用表值参数。 2.删除共享文件夹代码并直接使用SqlBulkCopy类。
为了让读者更好地理解究竟是什么被测试了,以消除对这些数据可靠性的任何疑虑,这里更详细地解释一下实际上进行的导入过程: 1.从通常包含约20-50个数据点的时间序列开始; 2.进行一系列的非常疯狂的处理,这些处理大多数与数据库无关。该过程并行化了,因此(1)中的8-10个序列正在同时被处理。每个并行进程会生成3个额外的序列; 3.将所有3个序列和原始序列合并成一个批次; 4.将所有8-10个已完成处理任务的批次组合成一个大型超级批次; 5.使用批量插入策略(请转到下一步)或TVP策略(跳到第8步)进行导入; 6.使用SqlBulkCopy类将整个超级批次倾倒到4个永久暂存表中; 7.运行一个存储过程,该存储过程(a)对两个表执行了一堆聚合步骤,包括多个JOIN条件,然后(b)对6个生产表执行了MERGE,同时使用了聚合和非聚合数据。(完毕) 或者: 8.生成4个包含要合并的数据的DataTable对象;其中3个包含CLR类型,不幸的是ADO.NET TVPs没有正确支持它们,因此必须将它们作为字符串表示形式塞入其中,这会稍微影响性能。 9.将TVP馈送到一个存储过程中,该存储过程直接使用接收到的表执行了基本相同的处理。(完毕)
结果相当接近,但平均而言,TVP方法的性能更好,即使数据略微超过1000行。请注意,此导入过程会连续运行很多次,因此通过计算完成所有合并所需的时间(是的,时间),很容易得出平均时间。
最初,一个平均合并大约需要8秒才能完成(在正常负载下)。去除 NetBIOS 麻烦和切换到 SqlBulkCopy 将时间减少到几乎完全的 7 秒。切换到 TVP 进一步将时间降低到每批约 5.2 秒。对于一个以小时为单位测量运行时间的流程来说,这是吞吐量提高了约 35%,效果不错。这也比 SqlBulkCopy 提高了约 25%。
实际上,我相当有信心真正的改进远远超过这个数字。在测试过程中,明显最后的合并已不再是关键路径;相反,正在处理所有数据的 Web 服务开始因请求数量太多而崩溃。CPU 和数据库 I/O 都没有达到极限,并且没有重要的锁定活动。在某些情况下,我们看到连续合并之间存在几秒钟的空闲时间。使用 SqlBulkCopy 时,有一些空隙,但要小得多(半秒左右)。但我想这将成为另一天的故事。
结论:对于操作中型数据集的复杂导入+转换过程,表值参数确实比 BULK INSERT 操作性能更好。
我想再添加一个观点,以消除那些支持分阶段表的人的任何担忧。在某种程度上,整个服务都是一个巨大的分阶段过程。过程的每个步骤都经过了严格审核,因此我们不需要一个分阶段表来确定为什么某个特定的合并失败(虽然在实践中几乎从不发生)。我们所要做的就是在服务中设置一个调试标志,它就会中断到调试器或将其数据转储到文件而不是数据库。
换句话说,我们已经有足够深入的了解过程,并不需要分阶段表的安全保障。之所以一开始有分阶段表,只是为了避免使用所有的 INSERT 和 UPDATE 语句时出现抖动。在原始过程中,暂存数据只在暂存表中存活了几分之一秒,因此在维护/可维护性方面没有增加任何价值。请注意,我们并没有用 TVP 替换每个 BULK INSERT 操作。一些处理更大量数据或不需要对数据执行任何特殊操作的操作仍使用 SqlBulkCopy。我并不是在建议 TVP 是性能万灵药,只是在这个涉及初始分段和最终合并之间的若干转换的具体实例中,TVP 在性能上胜过了 SqlBulkCopy。因此,这就是答案。虽然 TToni 找到了最相关的链接,但我也感谢其他人的回复。再次感谢!

这本身就是一个很棒的问题,我觉得更新部分应该在回答中。 - Marc.2377
4个回答

11

我并没有使用TVP的经验,但是MSDN提供了一份与BULK INSERT的性能比较表 (点击此处)

他们说BULK INSERT有更高的启动成本,但此后速度更快。在远程客户端方案中,对于“简单”服务器逻辑,他们将阈值设为约1000行。根据他们的描述,我认为使用TVP应该没问题。如果有任何性能损失,那可能可以忽略不计,而且架构上的好处似乎非常显著。

另外,你可以通过使用SqlBulkCopy对象来避免使用服务器本地文件,同时仍然使用批量复制功能。只需填充一个DataTable,并将其输入到SqlBulkCopy实例的“WriteToServer”方法中即可。易于使用,而且速度非常快。


谢谢提供链接,这实际上非常有用,因为微软似乎推荐在数据涉及复杂逻辑时使用TVPs(而我们也可以调整批处理大小,以避免超过1k行的瓶颈)。基于此,即使最终速度较慢,尝试一下也可能值得花费时间。 - Aaronaught
是的,这个链接很有趣。@Aaronaught - 在这种情况下,探索和分析潜在方法的性能总是非常值得的,所以我很想听听您的发现! - AdaTheDev

8
提到@TToni的答案中提供的链接,需要考虑到上下文。我不确定这些建议实际上有多少研究支持(还要注意,此图表似乎仅在该文档的20082008 R2版本中可用)。
另一方面,SQL Server客户咨询团队有一份白皮书:使用TVP最大化吞吐量 自2009年以来,我一直在使用TVP,并且发现,至少在我的经验中,对于除了插入到目标表中没有其他逻辑需求之外的任何情况(这很少是情况),TVP通常是更好的选择。
我倾向于避免使用分段表,因为数据验证应在应用程序层进行。通过使用TVP,可以轻松地实现这一点,并且存储过程中的TVP表变量本质上是一个本地分段表(因此不会与使用实际表进行分段时运行的其他进程发生冲突)。
关于问题中进行的测试,我认为可以显示比最初发现的速度更快:
1.除非您的应用程序在发送值到TVP之外还有用途,否则不应使用DataTable。使用IEnumerable<SqlDataRecord>接口更快并且使用的内存更少,因为您不会在内存中复制集合,只需将其发送到DB即可。我在以下位置记录了这一点:
- 如何在最短时间内插入1000万条记录?(这里还有很多额外的信息和链接) - 将Dictionary<string,int>传递给存储过程T-SQL - 从应用程序中流式传输数据到SQL Server 2008(在SQLServerCentral.com上;需要免费注册)
2.TVP是表变量,因此不维护统计信息。这意味着它们向查询优化器报告仅具有1行。因此,在您的存储过程中,可以:
- 对于除简单SELECT之外的任何使用TVP的查询,请在语句级别重新编译:OPTION (RECOMPILE) - 创建本地临时表(即单个#)并将TVP的内容复制到临时表中

看起来白皮书的链接已经失效了。 - undefined

6
我认为我仍然会坚持使用批量插入的方法。即使使用了具有合理行数的TVP,您可能会发现tempdb仍然会受到影响。这是我的直觉,我不能说我已经测试过使用TVP的性能(我也很想听取其他人的意见)。
您没有提到是否使用.NET,但我优化以前的解决方案所采取的方法是使用SqlBulkCopy类进行数据批量加载 - 您不需要先将数据写入文件再加载,只需给SqlBulkCopy类(例如)一个DataTable- 这是将数据插入DB的最快方式。 5-10K行不算多,我已经使用过750K行。我认为通常情况下,使用TVP处理几百行不会有太大差异。但在我的看法中,扩展规模会受到限制。

也许 SQL 2008 中的新 MERGE 功能会对您有所帮助?

此外,如果您现有的暂存表是一个单表,用于每个进程实例,并且担心争用等问题,是否考虑每次创建一个新的“临时”但物理暂存表,然后在完成后将其删除?

请注意,您可以通过在不添加索引的情况下填充它来优化加载到此暂存表中的数据。然后一旦填充完毕,在该点上添加任何所需的索引(FILLFACTOR=100 可获得最佳读取性能,因为此时它不会被更新)。


我确实使用.NET,这个过程恰好在SqlBulkCopy之前就存在了,并且从未更改过。谢谢你提醒我,这可能值得重新审视。MERGE也已经广泛使用,临时表以前尝试过,但发现速度较慢且更难管理。感谢您的建议! - Aaronaught

-3

暂存表是很好的!我真的不想用其他方法。为什么?因为数据导入可能会出现意外变化(而且通常是以你无法预见的方式,比如某些列仍然被称为名字和姓氏,但是名字数据在姓氏列中,例如,这只是一个随机的例子)。使用暂存表可以轻松地研究问题,因此您可以看到导入处理的列中确切的数据。当您使用内存表时,我认为更难找到。我认识很多像我一样从事导入工作的人,他们都建议使用暂存表。我怀疑这背后有原因。

进一步修复工作流程中的小型模式更改比重新设计流程更容易且耗时更少。如果它正在运行,而且没有人愿意支付时间来更改它,那么只需由于模式更改而修复需要修复的内容即可。通过更改整个流程,您引入了比对现有经过测试的工作流程进行小修改更多的潜在新错误。

那么,您打算如何消除所有数据清理任务呢?您可能会以不同的方式执行它们,但它们仍然需要完成。再次强调,按照您描述的方式更改流程非常危险。

个人认为,你只是因为使用旧技术而感到不满,而没有机会玩新玩具。你似乎没有真正的理由想要改变,除了批量插入已经过时了。


28
SQL 2008已经存在了2年,这个过程也已经存在了很长时间,但这是我第一次甚至考虑更改它。最后的尖刻评论真的有必要吗? - Aaronaught

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接