标识列上具有父/子关系的SqlBulkCopy和DataTables

我们需要根据父表中的Identity主键更新多个具有父/子关系的表,父表由一个或多个子表作为外键引用。

  • 由于数据量很大,我们希望在内存中构建这些表,然后使用C#中的SqlBulkCopy从DataSet或单个DataTables更新数据库。
  • 我们还希望从多个线程,进程和可能的客户端并行执行此操作。

我们在F#中的原型显示了很多承诺,性能提高了34倍,但是这段代码强制了父表中已知的Identity值。 如果没有强制,当SqlBulkCopy插入行时,Identity列会在数据库中正确生成,但Identity值不会在内存中的DataTable中更新。 此外,即使它们是,也不清楚DataSet是否能正确地修复父/子关系,以便随后可以用正确的外键值写入子表。

任何人都可以解释如何使用SqlBulkCopy更新标识值,以及如何配置数据集以保留和更新父/子关系,如果在单个DataTables上调用DataAdapter到FillSchema时不自动完成。

我不想要的答案:

  • 读取数据库以查找当前最高的Identity值,然后在创建每个父行时手动递增它。 不适用于多个进程/客户端,据我所知,失败的事务可能会导致某些标识值被跳过,因此这种方法可能会破坏关系。
  • 一次一个地写出父行,并要求返回Identity值。 通过使用SqlBulkCopy,这至少会破坏一些收益(是的,有多个子行比父类行多,但仍有很多父行)。

类似于以下未回答的问题:

  • 如何使用自动生成的标识密钥更新数据集父子表?

首先:SqlBulkCopy不可能做你想要的。 顾名思义,它只是一条“单行道”。 我尽可能快地将数据移动到sql server中。 它是旧的批量复制命令的.Net版本,它将原始文本文件导入表中。 因此,如果使用SqlBulkCopy,则无法获取标识值。

我做了很多批量数据处理,并且多次遇到过这个问题。 解决方案取决于您的体系结构和数据分布。 以下是一些想法:

  • 为每个线程创建一组目标表,在这些表中导入。 最后加入这些表格。 其中大多数可以通过一种非常通用的方式实现,您可以从名为TABLENAME的表中自动生成名为TABLENAME_THREAD_ID的表。

  • 将ID生成完全移出数据库。 例如,实现生成ID的中央Web服务。 在这种情况下,您不应该为每个调用生成一个ID,而是生成ID范围。 否则,网络开销通常会变成瓶颈。

  • 尝试为您的数据生成ID。 如果可能的话,你的问题就会消失。 不要说快“是不可能的”。 也许您可以使用可以在后处理步骤中清理的字符串ID?

还有一点评论:使用BulkCopy时,因子34的增加会增加。 如果要快速插入数据,请确保正确配置数据库。

阅读这篇文章。 我认为这正是您正在寻找的东西以及更多。 非常好,优雅的解决方案。

http://www.codinghelmet.com/?path=howto/bulk-insert

使用SqlBulkCopy可以执行所需操作的唯一方法是首先将数据插入到临时表中。 然后使用存储过程将数据分发到destinate表。 是的,这将导致减速,但仍然会很快。

您也可以考虑重新设计数据,即将其拆分,对其进行非规范化等。

set identity_insert

on

dbcc checkident是你的朋友。 这就像我过去所做的那样(参见代码示例)。 唯一真正需要注意的是,更新过程是唯一可以插入数据的过程:在更新过程中,其他所有人都必须离开池。 当然,您可以在加载生产表之前以编程方式执行此类映射。 但是对插入的限制同样适用:更新过程是唯一可以发挥作用的过程。

 -- -- start with a source schema -- doesn't actually need to be SQL tables -- but from the standpoint of demonstration, it makes it easier -- create table source.parent ( id int not null primary key , data varchar(32) not null , ) create table source.child ( id int not null primary key , data varchar(32) not null , parent_id int not null foreign key references source.parent(id) , ) -- -- On the receiving end, you need to create staging tables. -- You'll notice that while there are primary keys defined, -- there are no foreign key constraints. Depending on the -- cleanliness of your data, you might even get rid of the -- primary key definitions (though you'll need to add -- some sort of processing to clean the data one way or -- another, obviously). -- -- and, depending context, these could even be temp tables -- create table stage.parent ( id int not null primary key , data varchar(32) not null , ) create table stage.child ( id int not null primary key , data varchar(32) not null , parent_id int not null , ) -- -- and of course, the final destination tables already exist, -- complete with identity properties, etc. -- create table dbo.parent ( id int not null identity(1,1) primary key , data varchar(32) not null , ) create table dbo.child ( id int not null identity(1,1) primary key , data varchar(32) not null , parent_id int not null foreign key references dbo.parent(id) , ) ----------------------------------------------------------------------- -- so, you BCP or otherwise load your staging tables with the new data -- frome the source tables. How this happens is left as an exercise for -- the reader. We'll just assume that some sort of magic happens to -- make it so. Don't forget to truncate the staging tables prior to -- loading them with data. ----------------------------------------------------------------------- ------------------------------------------------------------------------- -- Now we get to work to populate the production tables with the new data -- -- First we need a map to let us create the new identity values. ------------------------------------------------------------------------- drop table #parent_map create table #parent_map ( old_id int not null primary key nonclustered , offset int not null identity(1,1) unique clustered , new_id int null , ) create table #child_map ( old_id int not null primary key nonclustered , offset int not null identity(1,1) unique clustered , new_id int null , ) insert #parent_map ( old_id ) select id from stage.parent insert #child_map ( old_id ) select id from stage.child ------------------------------------------------------------------------------- -- now that we've got the map, we can blast the data into the production tables ------------------------------------------------------------------------------- -- -- compute the new ID values -- update #parent_map set new_id = offset + ( select max(id) from dbo.parent ) -- -- blast it into the parent table, turning on identity_insert -- set identity_insert dbo.parent on insert dbo.parent (id,data) select id = map.new_id , data = staging.data from stage.parent staging join #parent_map map on map.old_id = staging.id set identity_insert dbo.parent off -- -- reseed the identity properties high water mark -- dbcc checkident dbo.parent , reseed -- -- compute the new ID values -- update #child_map set new_id = offset + ( select max(id) from dbo.child ) -- -- blast it into the child table, turning on identity_insert -- set identity_insert dbo.child on insert dbo.child ( id , data , parent_id ) select id = parent.new_id , data = staging.data , parent_id = parent.new_id from stage.child staging join #child_map map on map.old_id = staging.id join #parent_map parent on parent.old_id = staging.parent_id set identity_insert dbo.child off -- -- reseed the identity properties high water mark -- dbcc checkident dbo.child , reseed ------------------------------------ -- That's about all there is too it. ------------------------------------ 

我想你面临的折衷是BulkInsert的性能与Identity的可靠性。

您可以暂时将数据库放入SingleUserMode以执行插入吗?

我在转换项目中遇到了一个非常类似的问题,我在其中为非常大的表添加了一个Identity列,并且它们有子项。 幸运的是,我能够设置父和子源的身份(我使用TextDataReader)来执行BulkInsert,并且我同时生成了父文件和子文件。

我也获得了你所谈论的性能提升,OleDBDataReader Source – > StreamWriter …然后是TextDataReader – > SQLBulk