Массовая Вставка Dapper, Возвращающая Последовательные Идентификаторы
Я пытаюсь выполнить массовую вставку с помощью Dapper над Npgsql, который возвращает идентификаторы вновь вставленных строк. В обоих моих примерах используется следующий оператор insert:
var query = "INSERT INTO "MyTable" ("Value") VALUES (@Value) RETURNING "ID"";
во-первых, я попытался добавить массив объектов со свойством "Value":
var values = new[] {
new { Value = 0.0 },
new { Value = 0.5 }
};
var ids = connection.Query<int>(query, values);
однако это не удается с NpgsqlException: "ошибка: 42703: столбец" значение"не существует". После прочтения этот вопрос, Я подумал, что, возможно, мне придется пройти Объект DataTable вместо массива объектов:
var dataTable = new DataTable();
dataTable.Columns.Add("Value", typeof(double));
dataTable.Rows.Add(0.0);
dataTable.Rows.Add(0.5);
var ids = connection.Query<int>(query, dataTable);
однако это не удается с тем же исключением. Как я могу выполнить массовую вставку и получить результирующие последовательные идентификаторы из Dapper через Npgsql?
я заметил, что корпус исключения не соответствует имени столбца, но я уверен, что у меня есть кавычки вокруг имен таблиц и столбцов, поэтому я не уверен, почему он говорит "значение" вместо "значение" в исключении. Просто подумал, что упомяну об этом на всякий случай. это как-то связано с ошибкой, так как легко пропустить корпус.
-- EDIT --
чтобы уточнить, это SQL для создания таблицы
CREATE TABLE "MyTable" (
"ID" SERIAL PRIMARY KEY,
"Value" DOUBLE PRECISION NOT NULL
);
и используя переменные "query" и "values", определенные выше, это код, который работает на основе каждой строки:
var ids = new List<int>();
foreach (var valueObj in values) {
var queryParams = new DynamicParamaters();
queryParams.Add("Value", valueObj.Value);
ids.AddRange(connection.Query<int>(query, queryParams));
}
проблема в том, что мне нужно иметь возможность вставлять сотни (возможно, тысячи в ближайшем будущем) строк в секунду в "MyTable", поэтому ожидание этого цикла итеративно отправка каждого значения в базу данных является громоздкой и (я предполагаю, но еще не тест) трудоемкой. Кроме того, я выполняю дополнительные вычисления значений, которые могут или не могут привести к дополнительным вставкам, где мне нужна ссылка внешнего ключа на запись "MyTable".
из-за этих проблем я ищу альтернативу, которая отправляет все значения в одном операторе в базу данных, чтобы уменьшить сетевой трафик и задержку обработки. Опять же, я не сравнивал итеративный подход пока нет... то, что я ищу, - это альтернатива, которая делает массовую вставку, чтобы я мог сравнить два подхода друг с другом.
1 ответов
в конечном счете, я придумал четыре разных подхода к этой проблеме. Я создал 500 случайных значений для вставки в MyTable и синхронизировал каждый из четырех подходов (включая запуск и откат транзакции, в которой она была запущена). В моем тесте база данных находится на localhost. Однако для решения с лучшей производительностью также требуется только одна поездка туда и обратно на сервер базы данных, поэтому лучшее решение, которое я нашел, должно по-прежнему превзойти альтернативы при развертывании на другой сервер базы данных.
обратите внимание, что переменные connection
и transaction
используются в следующем коде и считаются допустимыми объектами данных Npgsql. Также обратите внимание, что обозначение Пх медленнее указывает, что операция заняла время, равное оптимальному решению, умноженному на N.
подход #1 (1,494 МС = 18,7 x медленнее): развернуть массив в отдельные параметры
public List<MyTable> InsertEntries(double[] entries)
{
// Create a variable used to dynamically build the query
var query = new StringBuilder(
"INSERT INTO \"MyTable\" (\"Value\") VALUES ");
// Create the dictionary used to store the query parameters
var queryParams = new DynamicParameters();
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add a unique parameter for each id
var paramIdx = 0;
foreach (var entry in result)
{
var paramName = string.Format("value{1:D6}", paramIdx);
if (0 < paramIdx++) query.Append(',');
query.AppendFormat("(:{0})", paramName);
queryParams.Add(paramName, entry.Value);
}
query.Append(" RETURNING \"ID\"");
// Execute the query, and store the ids
var ids = connection.Query<int>(query, queryParams, transaction);
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
я действительно не уверен, почему это вышло самым медленным, так как для этого требуется только одна поездка туда и обратно в базу данных, но это было.
подход #2 (267ms = 3.3 х медленнее): стандартная итерация цикла
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"INSERT INTO \"MyTable\" (\"Value\") VALUES (:val) RETURNING \"ID\"";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add each entry to the database
foreach (var entry in result)
{
var queryParams = new DynamicParameters();
queryParams.Add("val", entry.Value);
entry.ID = connection.Query<int>(
query, queryParams, transaction);
}
// Return the result
return result;
}
я был в шоке, что это было только 2x медленнее, чем оптимальное решение, но я ожидал бы, что это будет значительно хуже в реальной среде, так как это решение требует отправки 500 сообщений в сервер последовательно. Однако это также самое простое решение.
подход #3 (223ms = 2,8 х медленнее): асинхронные итерации цикла
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"INSERT INTO \"MyTable\" (\"Value\") VALUES (:val) RETURNING \"ID\"";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add each entry to the database asynchronously
var taskList = new List<Task<IEnumerable<int>>>();
foreach (var entry in result)
{
var queryParams = new DynamicParameters();
queryParams.Add("val", entry.Value);
taskList.Add(connection.QueryAsync<int>(
query, queryParams, transaction));
}
// Now that all queries have been sent, start reading the results
for (var i = 0; i < result.Count; ++i)
{
result[i].ID = taskList[i].Result.First();
}
// Return the result
return result;
}
это становится лучше, но все еще менее оптимальным, потому что мы можем только очередь столько вставок, сколько есть доступных потоков в пуле потоков. Однако это почти так же просто, как и непоточный подход, поэтому это хороший компромисс между скоростью и удобочитаемость.
подход #4 (134ms = 1.7 x медленнее): массовые вставки
этот подход требует, чтобы перед запуском сегмента кода под ним был определен следующий Postgres SQL:
CREATE TYPE "MyTableType" AS (
"Value" DOUBLE PRECISION
);
CREATE FUNCTION "InsertIntoMyTable"(entries "MyTableType"[])
RETURNS SETOF INT AS $$
DECLARE
insertCmd TEXT := 'INSERT INTO "MyTable" ("Value") '
'VALUES () RETURNING "ID"';
entry "MyTableType";
BEGIN
FOREACH entry IN ARRAY entries LOOP
RETURN QUERY EXECUTE insertCmd USING entry."Value";
END LOOP;
END;
$$ LANGUAGE PLPGSQL;
и соответствующий код:
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"SELECT * FROM \"InsertIntoMyTable\"(:entries::\"MyTableType\")";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Convert each entry into a Postgres string
var entryStrings = result.Select(
e => string.Format("({0:E16})", e.Value).ToArray();
// Create a parameter for the array of MyTable entries
var queryParam = new {entries = entryStrings};
// Perform the insert
var ids = connection.Query<int>(query, queryParam, transaction);
// Assign each id to the result
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
есть два вопроса, которые у меня есть с этим подходом. Во-первых, мне нужно жестко закодировать порядок членов MyTableType. Если этот порядок когда-либо изменится, я должен изменить это код совпадает. Во-вторых, я должен преобразовать все входные значения в строку перед отправкой их в postgres (в реальном коде у меня есть более одного столбца, поэтому я не могу просто изменить подпись функции базы данных, чтобы взять двойную точность [], если я не передаю N массивов, где N-количество полей на MyTableType).
несмотря на эти подводные камни, это становится ближе к идеалу, и требуется только один туда и обратно в базу данных.
-- НАЧАТЬ РЕДАКТИРОВАНИЕ --
С момента первоначального сообщения я придумал четыре дополнительных подхода, которые все быстрее, чем перечисленные выше. Я изменил Пх медленнее номера, чтобы отразить новый самый быстрый метод, ниже.
подход #5 (105ms = 1.3 x медленнее): то же, что и #4, без динамического запроса
единственная разница между этим подходом и подход #4 следующее изменение к " InsertIntoMyTable" функция:
CREATE FUNCTION "InsertIntoMyTable"(entries "MyTableType"[])
RETURNS SETOF INT AS $$
DECLARE
entry "MyTableType";
BEGIN
FOREACH entry IN ARRAY entries LOOP
RETURN QUERY INSERT INTO "MyTable" ("Value")
VALUES (entry."Value") RETURNING "ID";
END LOOP;
END;
$$ LANGUAGE PLPGSQL;
В дополнение к проблемам с подход #4, недостатком этого является то, что в производственной среде "MyTable" разбивается на разделы. Используя этот подход, мне нужен один метод на целевой раздел.
подход #6 (89ms = 1.1 x медленнее): Insert с параметром массива
public List<MyTable> InsertEntries(double[] entries)
{
const string query =
"INSERT INTO \"MyTable\" (\"Value\") SELECT a.* FROM " +
"UNNEST(:entries::\"MyTableType\") a RETURNING \"ID\"";
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Convert each entry into a Postgres string
var entryStrings = result.Select(
e => string.Format("({0:E16})", e.Value).ToArray();
// Create a parameter for the array of MyTable entries
var queryParam = new {entries = entryStrings};
// Perform the insert
var ids = connection.Query<int>(query, queryParam, transaction);
// Assign each id to the result
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
единственным недостатком этого является то же самое, что и первая проблема с подход #4. А именно, что это пары реализации к упорядочению "MyTableType"
. Тем не менее, я обнаружил, что это мой второй любимый подход, так как он очень быстрый и не требует каких-либо функций базы данных для правильной работы.
подход #7 (80ms = очень немного медленнее): то же, что и #1, но без параметров
public List<MyTable> InsertEntries(double[] entries)
{
// Create a variable used to dynamically build the query
var query = new StringBuilder(
"INSERT INTO \"MyTable\" (\"Value\") VALUES");
// Get the result set without auto-assigned ids
var result = entries.Select(e => new MyTable { Value = e }).ToList();
// Add each row directly into the insert statement
for (var i = 0; i < result.Count; ++i)
{
entry = result[i];
query.Append(i == 0 ? ' ' : ',');
query.AppendFormat("({0:E16})", entry.Value);
}
query.Append(" RETURNING \"ID\"");
// Execute the query, and store the ids
var ids = connection.Query<int>(query, null, transaction);
ids.ForEach((id, i) => result[i].ID = id);
// Return the result
return result;
}
это мой любимый подход. Он лишь незначительно медленнее самого быстрого (даже с 4000 записями он все еще работает менее 1 секунды), но не требует специального функции или типы баз данных. Единственное, что мне в нем не нравится, - это то, что я должен stringify значения двойной точности, только чтобы снова быть проанализированным Postgres. Было бы предпочтительнее отправить значения в двоичном формате, чтобы они заняли 8 байтов вместо массивных 20 или около того байтов, которые я выделил для них.
подход #8 (80ms): то же, что и #5, но в чистом sql
единственная разница между этим подходом и подход #5 - это следующее изменение функции "InsertIntoMyTable":
CREATE FUNCTION "InsertIntoMyTable"(
entries "MyTableType"[]) RETURNS SETOF INT AS $$
INSERT INTO "MyTable" ("Value")
SELECT a.* FROM UNNEST(entries) a RETURNING "ID";
$$ LANGUAGE SQL;
этот подход, как и #5, требует одной функции в "MyTable" раздел. Это самый быстрый, потому что план запроса может быть создан один раз для каждой функции, а затем повторно использован. В других подходах запрос должен быть проанализирован, затем спланирован, затем выполнен. Несмотря на то, что это самый быстрый, я не выбрал его из-за дополнительных требований со стороны базы данных подход #7, с очень небольшим преимуществом скорости.