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 找到了最相关的链接,但我也感谢其他人的回复。再次感谢!