Массовая Вставка 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, с очень небольшим преимуществом скорости.