26

我们需要根据父表中的 Identity 主键更新几个具有父/子关系的表,该主键被一个或多个子表称为外键。

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

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

谁能解释如何让 SqlBulkCopy 更新标识值,以及如何配置 DataSet 以保留和更新父/子关系,如果在单个 DataTables 上调用 DataAdapter 来 FillSchema 时没有自动完成。

我不想要的答案:

  • 读取数据库以找到当前最高的 Identity 值,然后在创建每个父行时手动递增它。不适用于多个进程/客户端,据我了解,失败的事务可能会导致某些身份值被跳过,因此这种方法可能会搞砸关系。
  • 一次写一个父行并要求返回标识值。这至少抵消了使用 SqlBulkCopy 所带来的一些好处(是的,子行比父行多得多,但仍然有很多父行)。

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

4

5 回答 5

10

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

我已经做了很多批量数据处理,并且多次遇到过这个问题。解决方案取决于您的架构和数据分布。这里有一些想法:

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

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

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

还有一句话:在使用 BulkCopy 时增加 34 倍的声音听起来很小。如果您想快速插入数据,请确保您的数据库配置正确。

于 2009-07-31T19:54:52.840 回答
4

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

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

于 2013-04-11T14:06:28.550 回答
1

set identity_insert <table> 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.
------------------------------------
于 2011-02-25T21:03:32.750 回答
1

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

您也可以考虑重新设计您的数据,即拆分、非规范化等。

于 2011-02-25T20:18:26.240 回答
0

我想你面临的权衡是 BulkInsert 的性能与 Identity 的可靠性。

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

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

我还获得了您所说的性能提升,OleDBDataReader Source -> StreamWriter ...然后是 TextDataReader -> SQLBulk

于 2009-07-29T07:44:18.887 回答