Можно вставить с параметром таблицы, а также получить значения идентичности?
Я пытаюсь вставить записи, используя высокопроизводительный метод таблицы параметров ( http://www.altdevblogaday.com/2012/05/16/sql-server-high-performance-inserts/), и мне интересно, если это можно получить обратно значения идентичности для каждой записи, которую я вставляю.
На данный момент ответом будет "нет" - я вставляю данные, затем извлекаю значения идентификаторов, и они не совпадают. В частности, они не соответствуют примерно 75% времени, и они не совпадают непредсказуемым образом. Вот некоторый код, который повторяет эту проблему:
// Create a datatable with 100k rows
DataTable dt = new DataTable();
dt.Columns.Add(new DataColumn("item_id", typeof(int)));
dt.Columns.Add(new DataColumn("comment", typeof(string)));
for (int i = 0; i < 100000; i++) {
dt.Rows.Add(new object[] { 0, i.ToString() });
}
// Insert these records and retrieve back the identity
using (SqlConnection conn = new SqlConnection("Data Source=localhost;Initial Catalog=testdb;Integrated Security=True")) {
conn.Open();
using (SqlCommand cmd = new SqlCommand("proc_bulk_insert_test", conn)) {
cmd.CommandType = CommandType.StoredProcedure;
// Adding a "structured" parameter allows you to insert tons of data with low overhead
SqlParameter param = new SqlParameter("@mytable", SqlDbType.Structured);
param.Value = dt;
cmd.Parameters.Add(param);
SqlDataReader dr = cmd.ExecuteReader();
// Set all the records' identity values
int i = 0;
while (dr.Read()) {
dt.Rows[i].ItemArray = new object[] { dr.GetInt32(0), dt.Rows[i].ItemArray[1] };
i++;
}
dr.Close();
}
// Do all the records' ID numbers match what I received back from the database?
using (SqlCommand cmd = new SqlCommand("SELECT * FROM bulk_insert_test WHERE item_id >= @base_identity ORDER BY item_id ASC", conn)) {
cmd.Parameters.AddWithValue("@base_identity", (int)dt.Rows[0].ItemArray[0]);
SqlDataReader dr = cmd.ExecuteReader();
DataTable dtresult = new DataTable();
dtresult.Load(dr);
}
}
База данных определяется с помощью этого сценария сервера SQL:
CREATE TABLE bulk_insert_test (
item_id int IDENTITY (1, 1) NOT NULL PRIMARY KEY,
comment varchar(20)
)
GO
CREATE TYPE bulk_insert_table_type AS TABLE ( item_id int, comment varchar(20) )
GO
CREATE PROCEDURE proc_bulk_insert_test
@mytable bulk_insert_table_type READONLY
AS
DECLARE @TableOfIdentities TABLE (IdentValue INT)
INSERT INTO bulk_insert_test (comment)
OUTPUT Inserted.item_id INTO @TableOfIdentities(IdentValue)
SELECT comment FROM @mytable
SELECT * FROM @TableOfIdentities
Вот проблема: значения, возвращаемые из proc_bulk_insert_test
не в том порядке, в котором были вставлены исходные записи. Поэтому я не могу программно назначить каждой записи item_id
значение, которое я получил от OUTPUT
заявление.
Кажется, единственное правильное решение SELECT
назад весь список записей, которые я только что вставил, но, честно говоря, я бы предпочел любое решение, которое уменьшило бы объем данных, передаваемых по сетевой карте моего SQL Server. У кого-нибудь есть лучшие решения для больших вставок, в то же время получая значения идентификаторов?
РЕДАКТИРОВАТЬ: Позвольте мне попытаться прояснить вопрос немного больше. Проблема в том, что я хотел бы, чтобы моя программа на C# узнала, какие значения идентичности SQL Server присвоены только что вставленным данным. Порядок не важен; но я хотел бы иметь возможность взять произвольный набор записей в C#, вставить их, используя метод параметров быстрой таблицы, а затем назначить их автоматически сгенерированные идентификационные номера в C#, не запрашивая всю таблицу обратно в память.
Учитывая, что это искусственный набор тестов, я попытался сжать его до как можно меньшего размера для читаемого кода. Позвольте мне описать, какие методы я использовал для решения этой проблемы:
- В моем исходном коде в приложении, из которого получен этот пример, я вставлял около 15 миллионов строк, используя 15 миллионов отдельных операторов вставки, получая обратно значение идентификатора после каждой вставки. Это работало, но было медленно.
- Я пересмотрел код, используя параметры таблицы высокой производительности для вставки. Затем я бы избавился от всех объектов в C# и прочитал из базы данных все объекты. Однако исходные записи содержали десятки столбцов с множеством значений varchar и десятичных чисел, поэтому этот метод требовал значительного сетевого трафика, хотя он был быстрым и работал.
- Теперь я начал исследование, чтобы выяснить, возможно ли использовать вставку параметра таблицы, и в то же время просить SQL Server просто сообщить значения идентификаторов. Я старался
scope_identity()
а такжеOUTPUT
но до сих пор не были успешными.
По сути, эта проблема была бы решена, если бы SQL Server всегда вставлял записи именно в том порядке, в котором я их предоставил. Можно ли сделать так, чтобы записи SQL-сервера вставлялись именно в том порядке, в котором они указаны в параметре вставки табличного значения?
РЕДАКТИРОВАТЬ 2: Этот подход кажется очень похожим на то, что Cade Roux цитирует ниже:
Однако в статье автор использует магическое уникальное значение "ProductNumber", чтобы соединить вставленную информацию из "выходного" значения с исходным параметром табличного значения. Я пытаюсь понять, как это сделать, если у моей таблицы нет волшебного уникального значения.
2 ответа
Ваш TVP - неупорядоченный набор, как обычный стол. Он имеет порядок, только когда вы указываете как таковой. Мало того, что у вас нет никакого способа указать фактический порядок здесь, вы также просто делаете SELECT * в конце без ORDER BY. Какой порядок вы ожидаете здесь? Вы фактически сказали SQL Server, что вам все равно. Тем не менее, я реализовал ваш код и без проблем вернул строки в правильном порядке. Я немного изменил процедуру, чтобы вы могли определить, какое значение идентификатора принадлежит какому комментарию:
DECLARE @TableOfIdentities TABLE (IdentValue INT, comment varchar(20))
INSERT INTO bulk_insert_test (comment)
OUTPUT Inserted.item_id, Inserted.comment
INTO @TableOfIdentities(IdentValue, comment)
SELECT comment FROM @mytable
SELECT * FROM @TableOfIdentities
Затем я назвал его с помощью этого кода (для этого нам не нужен весь C#):
DECLARE @t bulk_insert_table_type;
INSERT @t VALUES(5,'foo'),(2,'bar'),(3,'zzz');
SELECT * FROM @t;
EXEC dbo.proc_bulk_insert_test @t;
Результаты:
1 foo
2 bar
3 zzz
Если вы хотите убедиться, что выходные данные имеют порядок присвоения идентификаторов (что не обязательно соответствует "порядку", который имеет ваш неупорядоченный TVP), вы можете добавить ORDER BY item_id
до последнего выберите в вашей процедуре.
Если вы хотите вставить таблицу назначения так, чтобы значения вашей идентичности находились в важном для вас порядке, у вас есть несколько вариантов:
добавьте столбец к вашему TVP и вставьте порядок в этот столбец, затем используйте курсор для перебора строк в этом порядке и вставляйте по одной за раз. ИМХО, все же более эффективно, чем вызов всей процедуры для каждой строки.
добавьте столбец к вашему TVP, который указывает порядок, и используйте ORDER BY на вставке. Это не гарантируется, но относительно надежно, особенно если вы устраняете проблемы параллелизма с помощью MAXDOP 1.
В любом случае, вы, кажется, уделяете много внимания ORDER. Что на самом деле означает ваш заказ? Если вы хотите придать смысл определенному порядку, вам не следует делать это с помощью столбца IDENTITY.
Вы указываете нет ORDER BY
на этом: SELECT * FROM @TableOfIdentities
так что нет гарантии заказа. Если вы хотите, чтобы они были в том же порядке, в котором они были отправлены, выполните INNER JOIN
в том, что к данным, которые были вставлены с ORDER BY
соответствует порядку, в котором были отправлены строки.