bcp / BULK INSERT与表值参数的性能
我将不得不使用SQL Server的BULK INSERT
命令重写一些相当老的代码,因为架构已经改变了,而且我想到了也许我应该考虑用TVP切换到存储过程,但是我想知道它可能对性能有什么影响。
一些背景信息可能有助于解释为什么我问这个问题:
-
数据实际上是通过Web服务进入的。 Web服务将文本文件写入数据库服务器上的共享文件夹,然后执行
BULK INSERT
。 这个过程最初是在SQL Server 2000上实现的,当时真的没有别的办法,只能在服务器上夹几百个INSERT
语句,这实际上是原来的过程,并且是性能灾难。 -
数据被批量插入到永久登台表中,然后被合并到一个更大的表中(之后,它从登台表中被删除)。
-
要插入的数据量是“大”,但不是“巨大” – 通常是几百行,在极less数情况下可能是5-10k行。 因此,我的直觉是
BULK INSERT
是一个非日志操作,不会有太大的区别(当然,我不确定,因此是个问题)。 -
插入实际上是一个更大的stream水线批处理的一部分,需要连续发生多次; 因此性能至关重要。
我想用TVP取代BULK INSERT
的原因是:
-
通过NetBIOS编写文本文件可能已经花费了一些时间,从架构的angular度来看这是相当可怕的。
-
我相信舞台表可以(也应该)被淘汰。 主要原因是插入的数据需要在插入的同时用于其他更新,并且从大量生产表尝试更新比使用几乎空的暂存更昂贵表。 有了TVP,参数基本上就是临时表,我可以在主插入之前/之后做任何事情。
-
我几乎可以避免使用重复检查,清除代码以及所有与批量插入相关的开销。
-
如果服务器同时获取这些事务中的一些(我们试图避免它,但是它发生),则无需担心登台表或tempdb上的锁争用。
很明显,在将任何东西投入生产之前,我会先简单介绍一下,但是我认为在我花费所有时间之前先问问一下是个好主意,看看有没有人有严格的警告要求使用TVP来达到这个目的。
所以 – 对于那些对SQL Server 2008来说足够安逸的人来说,已经尝试过或者至less调查过这个,那么判断是什么? 对于插入数百行到数千行,频繁发生的事情,TVP是否切断了芥末? 与散装刀片相比,性能是否有显着差异?
更新:现在减less了92%的问号!
(又名:testing结果)
现在最终的结果是在36阶段的部署过程之后进行生产。 两种解决scheme都经过了广泛的testing
- 翻出共享文件夹代码并直接使用
SqlBulkCopy
类; - 使用TVP切换到存储过程。
为了让读者能够了解究竟是什么被testing的,为了消除对这些数据的可靠性的任何怀疑,下面是对这个导入过程实际上做了什么的更详细的解释:
-
从一个通常约20-50个数据点的时间数据序列开始(尽pipe有时可能多达几百个);
-
做一大堆疯狂的处理,大部分独立于数据库。 这个过程是并行的,所以(1)中的大约8-10个序列正在被同时处理。 每个并行进程生成3个附加序列。
-
把所有3个序列和原始序列合并成一个批次。
-
将所有8-10个已完成的加工任务的批次合并为一个大的超批次。
-
使用
BULK INSERT
策略(见下一步)或TVP策略(跳到步骤8)导入它。 -
使用
SqlBulkCopy
类将整个超批量转储到4个永久登台表中。 -
运行一个存储过程(a)在其中两个表上执行一系列聚合步骤,包括几个
JOIN
条件,然后(b)使用聚合数据和非聚合数据在6个生产表上执行MERGE
。 (成品)要么
-
生成包含要合并的数据的4个
DataTable
对象; 其中3个包含CLRtypes,不幸的是ADO.NET TVPs不能很好地支持这种types的CLRtypes,所以它们不得不作为stringexpression式来使用,这会使性能稍微受损。 -
将TVP提供给一个存储过程,它执行与(7)基本相同的处理,但直接与接收的表进行交互。 (成品)
结果是相当接近的,但是即使数据超过1000行less量,TVP方法的平均performance也最好。
请注意,这个导入过程连续运行了上千次,所以只需要计算完成所有合并所花费的时间(是,小时)就可以很容易地获得平均时间。
最初,平均合并花费了将近8秒完成(在正常负载下)。 删除NetBIOS kludge并切换到SqlBulkCopy
减less了几乎7秒的时间。 切换到TVP进一步减less了每批5.2秒的时间。 对于运行时间以小时为单位的stream程来说,吞吐量提高了35% – 所以一点也不差。 与SqlBulkCopy
相比,这也有了25%的提升。
我确实相当确信,真正的改善远远超过这个。 在testing过程中,显然最终的合并不再是关键path。 相反,正在进行所有数据处理的Web服务在进入的请求数量下开始出现问题.CPU和数据库I / O都没有真正被刷新,并且没有显着的locking活动。 在某些情况下,我们在连续合并之间看到了一些空闲时间的差距。 使用SqlBulkCopy
时有一个小小的差距,但要小得多(半秒左右)。 但我想这将成为另一天的故事。
结论: 对于在中型数据集上运行的复杂导入+转换过程,表值参数确实比BULK INSERT
操作性能更好。
我想补充一点,只是为了减轻对亲台的一部分人的担忧。 从某种意义上说,这整个服务是一个巨大的分期过程。 该stream程的每一步都经过严格审核,因此我们不需要临时表来确定为什么某些特定的合并失败(尽pipe在实践中几乎从不发生)。 我们所要做的就是在服务中设置一个debugging标志,它将打断debugging器或将其数据转储到文件而不是数据库。
换句话说,我们已经对stream程有了足够的了解,并且不需要临时表的安全; 我们暂存表的首要原因是为了避免在所有INSERT
和UPDATE
语句上出现颠倒,否则我们不得不使用这些语句。 在原始进程中,登台数据只能在临时表中生活几分之一秒,因此在维护/可维护性方面没有任何价值。
另外请注意,我们还没有用TVPsreplace每一个BULK INSERT
操作。 处理大量数据和/或不需要对数据做任何特殊处理的几个操作,除了将其扔到数据库之外,仍然使用SqlBulkCopy
。 我并不是说TVPs是一个performance灵丹妙药,只是在这个特定的例子中,他们成功地通过了SqlBulkCopy
,涉及到初始分段和最终合并之间的几个转换。
所以你有它。 点去TTonifind最相关的链接,但我也赞赏其他答复。 再次感谢!
我还没有真正的TVP经验,但是在这里 MSDN有一个很好的性能比较图表和BULK INSERT。
他们说,BULK INSERT的启动成本更高,但之后更快。 在远程客户端场景中,他们绘制了大约1000行(对于“简单”服务器逻辑)。 从他们的描述来看,我会说你使用TVP应该没问题。 性能受到影响 – 如果有的话 – 可能可以忽略不计,build筑效益看起来非常好。
编辑:在旁边注意您可以避免服务器本地文件,并仍然使用SqlBulkCopy对象使用批量复制。 只需填充一个DataTable,并将其提供给“WriteToServer” – 一个SqlBulkCopy实例的方法。 易于使用,速度非常快。
我想我仍然坚持使用批量插入的方法。 您可能会发现tempdb仍然使用具有合理行数的TVP命中。 这是我的直觉,我不能说我testing了使用TVP的performance(虽然我也有兴趣听到别人的意见)
你没有提到你是否使用了.NET,但是我采用的方法来优化以前的解决scheme是使用SqlBulkCopy类来完成大量的数据加载 – 你不需要先把数据写入文件加载,只要给SqlBulkCopy类(例如)一个DataTable – 这是插入数据到数据库的最快方式。 5-10K行并不多,我已经用了750K行。 我怀疑,一般来说,有几百行,使用TVP不会有太大的区别。 但扩大规模将是有限的恕我直言。
也许SQL 2008中新的MERGEfunction会使你受益?
另外,如果现有的登台表是一个用于此进程的每个实例的单个表,并且您担心争用等问题,是否考虑每次都创build一个新的“临时”但是物理登台表,然后在完成了?
请注意,您可以优化加载到此临时表,通过填充它没有任何索引。 然后一旦填充,在这一点上添加任何所需的索引(FILLFACTOR = 100为最佳的读取性能,因为在这一点上它不会被更新)。
关于@Toni的回答中提到的链接的图表需要在上下文中进行。 我不确定这些build议有多less实际的研究(还要注意图表似乎只在该文档的2008
和2008 R2
版本中可用)。
另一方面,SQL Server客户咨询团队发布了这个白皮书: 通过TVP最大化吞吐量
自2009年以来,我一直在使用TVP,至less以我的经验发现,除了简单地插入到目标表中而没有额外的逻辑需求(很less有这种情况),TVP通常是更好的select。
我倾向于避免登台,因为数据validation应该在应用层完成。 通过使用TVP,这很容易被容纳,并且存储过程中的TVP表variables本质上是一个本地化的登台表(因此与使用真实表进行登台时得到的其他进程不会同时运行) )。
关于在这个问题上所做的testing,我认为它可能比最初发现的更快:
- 您不应该使用DataTable,除非您的应用程序在将值发送到TVP之外使用它。 使用
IEnumerable<SqlDataRecord>
接口速度更快,占用的内存也更less,因为不会将内存中的集合复制到数据库中。 我有这样的logging在下列地方:- 如何在最短的时间内插入1000万条logging? (很多额外的信息和链接在这里以及)
- 将Dictionary <string,int>传递给存储过程T-SQL
- 从应用程序将数据stream式传输到SQL Server 2008 (在SQLServerCentral.com上;需要免费注册)
- TVP是表variables,因此不保留统计。 意思是,他们只向查询优化器报告1行。 所以,在你的过程中,要么:
- 对任何使用TVP的查询使用语句级重新编译,除了一个简单的SELECT:
OPTION (RECOMPILE)
- 创build一个本地临时表(即单个
#
),并将TVP的内容复制到临时表中
- 对任何使用TVP的查询使用语句级重新编译,除了一个简单的SELECT:
舞台表很好! 真的,我不想做任何其他的方式。 为什么? 因为数据导入可能会意外地发生变化(通常情况下,您无法预见到的情况,比如列名仍然被称为名字和姓氏,但在姓氏列中包含名字数据,例如,随便。)易于研究与登台表问题,所以你可以看到什么数据是在导入处理的列。 当你在内存表中使用时,很难find我想的。 我知道很多人为了生活而import,所有人都推荐使用登台表。 我怀疑这是有原因的。
进一步修改小的模式更改到工作过程比重新devise过程更容易,更省时。 如果它正在工作,而且没有人愿意花费几个小时来改变它,那么只需要修改由于模式改变而需要修改的东西。 通过改变整个过程,与通过对现有的经过testing的工作stream程进行小的改变相比,您会引入更多潜在的新bug。
而且,你将如何解决所有的数据清理任务? 你可能会以不同的方式做,但仍然需要完成。 再一次地,按照你描述的方式来改变这个过程是非常危险的。
就我个人而言,听起来像刚刚使用旧技术而不是冒险玩新玩具, 你似乎没有真正的基础想要改变,除了批量插入是如此2000年。