SqlBulkCopy и DataTables с соотношением родительских и дочерних элементов в столбце идентичности
Нам нужно обновить несколько таблиц, которые имеют родительские/дочерние отношения на основе первичного ключа Identity в родительской таблице, который упоминается одной или несколькими дочерними таблицами в качестве внешнего ключа.
- Из-за большого объема данных мы хотели бы построить эти таблицы в памяти, а затем использовать SqlBulkCopy из С# для обновления массива базы данных из DataSet или отдельных DataTables.
- Мы также хотели бы сделать это параллельно, из нескольких потоков, процессов и, возможно, клиентов.
Наш прототип в F # показывает многообещающие результаты с увеличением производительности 34 раза, но этот код заставляет использовать известные значения Identity в родительской таблице. Если не принудительно, столбец Identity корректно генерируется в базе данных, когда SqlBulkCopy вставляет строки, но значения Identity НЕ обновляются в встроенной памяти DataTable. Кроме того, даже если бы они были, неясно, правильно ли DataSet исправит отношения родительский/дочерний, чтобы потом дочерние таблицы могли быть записаны с правильными значениями внешнего ключа.
Может ли кто-нибудь объяснить, как обновить значения идентификаторов SqlBulkCopy, а также как настроить DataSet, чтобы сохранить и обновить отношения между родителями и дочерними элементами, если это не делается автоматически, когда DataAdapter вызывается в FillSchema на отдельных таблицах данных.
Ответы, которые я не ищу:
- Прочитайте базу данных, чтобы найти текущее самое высокое значение Identity, а затем вручную увеличивайте ее при создании каждой родительской строки. Не работает для нескольких процессов/клиентов, и, насколько я понимаю, неудачные транзакции могут привести к пропущению некоторых значений Identity, поэтому этот метод может испортить отношение.
- Напишите родительские строки по очереди и попросите вернуть значение Identity. Это побеждает, по крайней мере, в некоторых преимуществах, используя SqlBulkCopy (да, есть намного больше дочерних строк, чем у родителей, но все еще много родительских строк).
Аналогично следующему неотвеченному вопросу:
Ответы
Ответ 1
Прежде всего: SqlBulkCopy не может делать то, что вы хотите. Как следует из названия, это просто "улица в одну сторону". Я перемещаю данные в sql-сервер как можно быстрее. Это .Net-версия старой команды массового копирования, которая импортирует необработанные текстовые файлы в таблицы. Таким образом, нет способа вернуть значения идентификатора, если вы используете SqlBulkCopy.
Я много обрабатывал объемную обработку данных и несколько раз сталкивался с этой проблемой. Решение зависит от вашей архитектуры и распределения данных. Вот несколько идей:
-
Создайте один набор целевых таблиц для каждого потока, импортируйте в эти таблицы. В конце соедините эти таблицы. Большинство из них могут быть реализованы довольно общим способом, когда вы автоматически генерируете таблицы TABLENAME_THREAD_ID из таблиц, называемых TABLENAME.
-
Полностью переместить создание идентификатора из базы данных. Например, реализуйте центральный веб-сервис, который генерирует идентификаторы. В этом случае вы не должны генерировать один идентификатор для каждого вызова, а скорее генерируете диапазоны идентификаторов. В противном случае сетевые накладные расходы обычно становятся шеей бутылки.
-
Попробуйте генерировать идентификаторы ваших данных. Если возможно, ваша проблема исчезла бы. Не говорите "не возможно" быстро. Возможно, вы можете использовать идентификаторы строк, которые можно очистить на этапе последующей обработки?
И еще одно замечание: увеличение коэффициента 34 при использовании BulkCopy звучит малозначительно. Если вы хотите быстро вставлять данные, убедитесь, что ваша база данных настроена правильно.
Ответ 2
Прочтите эту статью. Я думаю, что это именно то, что вы ищете и многое другое. Очень приятное и элегантное решение.
http://www.codinghelmet.com/?path=howto/bulk-insert
Ответ 3
Единственный способ сделать то, что вы хотите, используя SqlBulkCopy, - это сначала вставить данные в промежуточную таблицу. Затем используйте хранимую процедуру для распространения данных в таблицы destinate. Да, это приведет к замедлению, но все равно будет быстро.
Вы также можете подумать о перепроектировании ваших данных, то есть расщепить его, денормализовать его и т.д.
Ответ 4
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 about all there is too it.
------------------------------------
Ответ 5
Я полагаю, что компромисс с вами - это производительность BulkInsert против надежности Identity.
Можете ли вы временно поместить базу данных в SingleUserMode для выполнения вашей вставки?
У меня возникла очень похожая проблема с моим проектом конверсии, где я добавляю столбец Identity к очень большим таблицам, и у них есть дети. К счастью, я смог установить идентификатор родительского и дочернего источников (я использовал TextDataReader) для выполнения BulkInsert, и одновременно создал родительский и дочерний файлы.
Я также получил прирост производительности, о котором вы говорите, OleDBDataReader Source → StreamWriter..., а затем TextDataReader → SQLBulk