85

我将不得不使用 SQL Server 的BULK INSERT命令重写一些相当旧的代码,因为架构已经改变,我想到也许我应该考虑改用 TVP 切换到存储过程,但我想知道什么效果它可能对性能有影响。

一些背景信息可能有助于解释我为什么要问这个问题:

  • 数据实际上是通过 Web 服务传入的。Web 服务将一个文本文件写入数据库服务器上的共享文件夹,该文件夹依次执行BULK INSERT. 这个过程最初是在 SQL Server 2000 上实现的,当时除了在服务器上扔几百条语句之外真的别无选择INSERT,这实际上是原始过程,并且是性能灾难。

  • 数据被批量插入到永久暂存表中,然后合并到一个更大的表中(之后从暂存表中删除)。

  • 要插入的数据量是“大”的,但不是“巨大的”——通常是几百行,在极少数情况下可能会超过 5-10k 行。因此,我的直觉是,BULK INSERT作为一个非日志操作不会有那么大的区别(但我当然不确定,因此是这个问题)。

  • 插入实际上是一个更大的流水线批处理过程的一部分,需要连续发生多次;因此性能至关重要

我想BULK INSERT用 TVP 替换的原因是:

  • 在 NetBIOS 上编写文本文件可能已经花费了一些时间,而且从体系结构的角度来看是相当可怕的。

  • 我相信临时表可以(并且应该)被淘汰。它存在的主要原因是插入的数据需要在插入的同时用于其他几个更新,并且尝试从大量生产表中进行更新比使用几乎为空的 staging 成本要高得多桌子。使用 TVP,参数基本上临时表,我可以在主插入之前/之后用它做任何我想做的事情。

  • 我几乎可以消除重复检查、清理代码以及与批量插入相关的所有开销。

  • 如果服务器一次获得一些此类事务,则无需担心临时表或 tempdb 上的锁争用(我们试图避免它,但它确实发生了)。

我显然会在将任何东西投入生产之前对此进行分析,但我认为在我花费所有时间之前先询问一下可能是个好主意,看看是否有人有任何关于为此目的使用 TVP 的严厉警告。

所以 - 对于任何对 SQL Server 2008 足够熟悉并尝试过或至少对此进行过调查的人来说,结论是什么?比如说,几百到几千行的插入,在相当频繁的基础上发生,TVP 会减少芥末吗?与批量插入相比,性能是否存在显着差异?


更新:现在问号减少了 92%!

(又名:测试结果)

经过 36 个阶段的部署过程后,最终结果现已投入生产。两种解决方案都经过了广泛的测试:

  • 删除共享文件夹代码并SqlBulkCopy直接使用该类;
  • 使用 TVP 切换到存储过程。

为了让读者可以了解究竟测试了什么,以消除对这些数据可靠性的怀疑,这里是这个导入过程实际作用的更详细的解释:

  1. 从通常有 20-50 个数据点的时间数据序列开始(尽管有时可能多达几百个);

  2. 对它进行一大堆疯狂的处理,这些处理大多独立于数据库。这个过程是并行的,因此(1)中大约有 8-10 个序列同时被处理。每个并行进程生成 3 个附加序列。

  3. 取所有 3 个序列和原始序列并将它们组合成一个批次。

  4. 将所有 8-10 个现已完成的处理任务中的批次组合成一个大的超级批次。

  5. 使用BULK INSERT策略(参见下一步)或 TVP 策略(跳至步骤 8)导入它。

  6. 使用SqlBulkCopy该类将整个超级批次转储到 4 个永久暂存表中。

  7. 运行一个存储过程,它 (a) 对 2 个表执行一系列聚合步骤,包括几个JOIN条件,然后 (b)MERGE使用聚合数据和非聚合数据对 6 个生产表执行 a。(完成的)

    或者

  8. 生成4个DataTable包含待合并数据的对象;其中 3 个包含 CLR 类型,遗憾的是 ADO.NET TVP 没有正确支持这些类型,因此必须将它们作为字符串表示形式插入,这会稍微损害性能。

  9. 将 TVP 馈送到存储过程,该过程与 (7) 进行基本相同的处理,但直接使用接收到的表。(完成的)

结果相当接近,但 TVP 方法最终平均表现更好,即使数据稍微超过 1000 行也是如此。

请注意,此导入过程连续运行数千次,因此只需计算完成所有合并所需的时间(是的,小时),就很容易获得平均时间。

最初,平均合并几乎需要 8 秒才能完成(在正常负载下)。删除 NetBIOS 杂物并切换到SqlBulkCopy将时间减少到几乎正好 7 秒。切换到 TVP 进一步将时间减少到每批5.2 秒。对于一个运行时间以小时为单位的进程来说,这提高了35%的吞吐量——所以一点也不差。它也比SqlBulkCopy.

我实际上相当有信心,真正的改进远不止于此。在测试过程中,很明显最终合并不再是关键路径。相反,执行所有数据处理的 Web 服务开始因请求的数量而崩溃。CPU 和数据库 I/O 都没有真正达到极限,也没有明显的锁定活动。在某些情况下,我们看到连续合并之间有几秒钟的空闲时间。使用时有一点差距,但要小得多(半秒左右)SqlBulkCopy。但我想这将成为另一天的故事。

结论:表值参数确实比BULK INSERT在中型数据集上运行的复杂导入+转换过程执行得更好。


我想补充一点,只是为了减轻部分支持登台的人的疑虑。在某种程度上,这整个服务是一个巨大的分期过程。该过程的每一步都经过严格审核,因此我们不需要临时表来确定某些特定合并失败的原因(尽管实际上它几乎从未发生过)。我们所要做的就是在服务中设置一个调试标志,它将中断调试器或将其数据转储到文件而不是数据库中。

换句话说,我们已经对流程有足够的了解,不需要临时表的安全性;我们首先拥有临时表的唯一原因是避免在所有我们必须使用的INSERT和语句上颠簸。UPDATE在最初的过程中,暂存数据在暂存表中只存在几分之一秒,因此在维护/可维护性方面没有增加任何价值。

另请注意,我们并没有用 TVP替换每个BULK INSERT操作。处理大量数据和/或不需要对数据做任何特殊处理(除了将其扔到数据库之外)的几个操作仍然使用SqlBulkCopy. 我并不是说 TVP 是一种性能灵丹妙药,只是说它们SqlBulkCopy在这个特定的实例中成功了,涉及初始阶段和最终合并之间的几次转换。

所以你有它。指向 TToni 寻找最相关的链接,但我也很欣赏其他回复。再次感谢!

4

4 回答 4

10

我还没有真正的 TVP 经验,但是这里有一个很好的性能比较图表与 MSDN 中的 BULK INSERT

他们说 BULK INSERT 的启动成本更高,但之后速度更快。在远程客户端场景中,他们在大约 1000 行处画线(对于“简单”服务器逻辑)。从他们的描述来看,我会说您应该可以使用 TVP。性能影响(如果有的话)可能可以忽略不计,而且架构优势似乎非常好。

编辑:附带说明,您可以通过使用 SqlBulkCopy 对象避免使用服务器本地文件并仍然使用大容量复制。只需填充一个 DataTable,并将其提供给 SqlBulkCopy 实例的“WriteToServer”方法。易于使用,而且速度非常快。

于 2010-01-27T20:25:17.797 回答
7

关于@TToni 答案中提供的链接的图表需要结合上下文来考虑。我不确定对这些建议进行了多少实际研究(另请注意,该图表似乎仅在该文档20082008 R2版本中可用)。

另一方面,SQL Server 客户咨询团队提供了这份白皮书:使用 TVP 最大化吞吐量

自 2009 年以来,我一直在使用 TVP,并且发现,至少根据我的经验,除了简单地插入目标表而没有额外的逻辑需求(这种情况很少发生)之外,TVP 通常是更好的选择。

我倾向于避免临时表,因为数据验证应该在应用层完成。通过使用 TVP,这很容易适应,并且存储过程中的 TVP 表变量本质上是一个本地化的暂存表(因此不会像使用真实表进行暂存时那样与同时运行的其他进程发生冲突)。

关于问题中完成的测试,我认为它可以被证明比最初发现的更快:

  1. 您不应该使用 DataTable,除非您的应用程序在将值发送到 TVP 之外还使用了它。使用该IEnumerable<SqlDataRecord>接口更快并且使用更少的内存,因为您不会复制内存中的集合只是为了将其发送到数据库。我在以下地方记录了这一点:
  2. TVP 是表变量,因此不维护统计信息。意思是,他们向查询优化器报告只有 1 行。所以,在你的过程中,要么:
    • 对使用 TVP 的任何查询使用语句级重新编译,而不是简单的 SELECT:OPTION (RECOMPILE)
    • 创建本地临时表(即 single #)并将 TVP 的内容复制到临时表中
于 2015-02-24T18:14:38.767 回答
5

我想我仍然会坚持使用批量插入方法。您可能会发现 tempdb 仍然会使用具有合理行数的 TVP 来命中。这是我的直觉,我不能说我已经测试过使用 TVP 的性能(不过我也有兴趣听到其他人的意见)

您没有提到您是否使用 .NET,但我为优化以前的解决方案所采用的方法是使用SqlBulkCopy类进行大量数据加载——您不需要先将数据写入文件加载,只需给SqlBulkCopy类(例如)一个 DataTable - 这是将数据插入数据库的最快方法。5-10K 行并不多,我已经将它用于多达 750K 行。我怀疑一般来说,如果有几百行,使用 TVP 不会有太大的不同。但扩大规模将是有限的恕我直言。

也许 SQL 2008 中的新MERGE功能会让您受益?

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

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

于 2010-01-27T20:19:26.100 回答
-2

分期表很好!真的,我不想以任何其他方式做到这一点。为什么?因为数据导入可能会意外更改(并且通常以您无法预见的方式,例如列仍被称为名字和姓氏但在姓氏列中具有名字数据的时间,例如,选择一个不随机。)使用临时表很容易研究问题,因此您可以准确查看导入处理的列中的数据。当您使用内存表时,我认为更难找到。我认识很多像我一样以进口为生的人,他们都建议使用临时表。我怀疑这是有原因的。

与重新设计流程相比,进一步修复工作流程的小架构更改更容易且耗时更少。如果它正在工作并且没有人愿意支付数小时来更改它,那么只修复由于架构更改而需要修复的内容。通过更改整个流程,您引入的潜在新错误比对现有的、经过测试的工作流程进行微小更改要多得多。

您将如何取消所有数据清理任务?您可能会以不同的方式执行它们,但它们仍然需要完成。同样,按照您描述的方式更改流程是非常冒险的。

就我个人而言,听起来你只是因为使用旧技术而不是有机会玩新玩具而被冒犯了。除了bulk insert 2000 之外,您似乎没有真正想要更改的依据。

于 2010-01-27T21:56:54.893 回答