web-dev-qa-db-ja.com

Dapperを使用すると、一括挿入に予想よりも時間がかかる

この記事 を読んだ後、Dapperの使用方法を詳しく調べることにしました。

空のデータベースでこのコードを実行しました

var members = new List<Member>();
for (int i = 0; i < 50000; i++)
{
    members.Add(new Member()
    {
        Username = i.toString(),
        IsActive = true
    });
}

using (var scope = new TransactionScope())
{
    connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);

    scope.Complete();
}

約20秒かかりました。これは2500挿入/秒です。悪いことではありませんが、ブログが1秒あたり45kの挿入を達成していることを考えると、素晴らしいことでもありません。 Dapperでこれを行うより効率的な方法はありますか?

また、副次的な注意として、このコードをVisual Studioデバッガーで実行すると、3分以上かかりました!私はそんなに見て本当に驚きました。

[〜#〜] update [〜#〜]

したがって、この

using (var scope = new TransactionScope())
{
    connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);

    scope.Complete();
}

この

    connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);

両方とも20秒かかりました。

しかし、これには4秒かかりました!

SqlTransaction trans = connection.BeginTransaction();

connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members, transaction: trans);

trans.Commit();
63
kenwarner

私が達成できた最高の方法は、このアプローチを使用して4秒で5万件のレコードを記録することでした。

SqlTransaction trans = connection.BeginTransaction();

connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members, transaction: trans);

trans.Commit();
70
kenwarner

私は最近これに出くわし、接続が開かれた後にTransactionScopeが作成されることに気付きました(クエリとは異なり、Dappers Executeは接続を開かないため、これを想定しています)。ここでの回答Q4によると、 https://stackoverflow.com/a/2886326/455904 TransactionScopeによって接続が処理されることはありません。私の同僚はいくつかの簡単なテストを行い、TransactionScopeの外部で接続を開くとパフォーマンスが大幅に低下しました。

したがって、次のように変更しても機能するはずです。

// Assuming the connection isn't already open
using (var scope = new TransactionScope())
{
    connection.Open();
    connection.Execute(@"
insert Member(Username, IsActive)
values(@Username, @IsActive)", members);

    scope.Complete();
}
11
Fredrik Ljung

Executeメソッドを1つのinsertステートメントのみで使用すると、一括挿入が行われず、効率的でもありません。 Transactionで受け入れられた回答でさえ、Bulk Insertを行いません。

Bulk Insertを実行する場合は、SqlBulkCopyhttps://msdn.Microsoft.com/en-us/library/system.data.sqlclient.sqlbulkcopyを使用します

これより速いものは何も見つかりません。

ダッパープラス

免責事項:私はプロジェクトの所有者です Dapper Plus

このプロジェクトは無料ではありませんが、すべての一括操作を提供します。

  • BulkInsert
  • BulkUpdate
  • BulkDelete
  • BulkMerge

(フードの下で使用SqlBulkCopy

そして、ID値の出力などのいくつかのオプション:

// CONFIGURE & MAP entity
DapperPlusManager.Entity<Order>()
                 .Table("Orders")
                 .Identity(x => x.ID);

// CHAIN & SAVE entity
connection.BulkInsert(orders)
          .AlsoInsert(order => order.Items);
          .Include(x => x.ThenMerge(order => order.Invoice)
                         .AlsoMerge(invoice => invoice.Items))
          .AlsoMerge(x => x.ShippingAddress);   

私たちのライブラリは複数のプロバイダーをサポートしています:

  • SQLサーバー
  • SQLコンパクト
  • オラクル
  • MySql
  • PostgreSQL
  • SQLite
  • 火の鳥
0
Jonathan Magnan

一括挿入を非常に迅速に実行できる拡張メソッドを作成しました。

public static class DapperExtensions
{
    public static async Task BulkInsert<T>(
        this IDbConnection connection,
        string tableName,
        IReadOnlyCollection<T> items,
        Dictionary<string, Func<T, object>> dataFunc)
    {
        const int MaxBatchSize = 1000;
        const int MaxParameterSize = 2000;

        var batchSize = Math.Min((int)Math.Ceiling((double)MaxParameterSize / dataFunc.Keys.Count), MaxBatchSize);
        var numberOfBatches = (int)Math.Ceiling((double)items.Count / batchSize);
        var columnNames = dataFunc.Keys;
        var insertSql = $"INSERT INTO {tableName} ({string.Join(", ", columnNames.Select(e => $"[{e}]"))}) VALUES ";
        var sqlToExecute = new List<Tuple<string, DynamicParameters>>();

        for (var i = 0; i < numberOfBatches; i++)
        {
            var dataToInsert = items.Skip(i * batchSize)
                .Take(batchSize);
            var valueSql = GetQueries(dataToInsert, dataFunc);

            sqlToExecute.Add(Tuple.Create($"{insertSql}{string.Join(", ", valueSql.Item1)}", valueSql.Item2));
        }

        foreach (var sql in sqlToExecute)
        {
            await connection.ExecuteAsync(sql.Item1, sql.Item2, commandTimeout: int.MaxValue);
        }
    }

    private static Tuple<IEnumerable<string>, DynamicParameters> GetQueries<T>(
        IEnumerable<T> dataToInsert,
        Dictionary<string, Func<T, object>> dataFunc)
    {
        var parameters = new DynamicParameters();

        return Tuple.Create(
            dataToInsert.Select(e => $"({string.Join(", ", GenerateQueryAndParameters(e, parameters, dataFunc))})"),
            parameters);
    }

    private static IEnumerable<string> GenerateQueryAndParameters<T>(
        T entity,
        DynamicParameters parameters,
        Dictionary<string, Func<T, object>> dataFunc)
    {
        var paramTemplateFunc = new Func<Guid, string>(guid => $"@p{guid.ToString().Replace("-", "")}");
        var paramList = new List<string>();

        foreach (var key in dataFunc)
        {
            var paramName = paramTemplateFunc(Guid.NewGuid());
            parameters.Add(paramName, key.Value(entity));
            paramList.Add(paramName);
        }

        return paramList;
    }
}

次に、この拡張メソッドを使用するには、次のようなコードを記述します。

await dbConnection.BulkInsert(
    "MySchemaName.MyTableName",
    myCollectionOfItems,
    new Dictionary<string, Func<MyObjectToInsert, object>>
        {
            { "ColumnOne", u => u.ColumnOne },
            { "ColumnTwo", u => u.ColumnTwo },
            ...
        });

これは非常に原始的であり、トランザクションやcommandTimeout値を渡すなど、改善の余地がありますが、私にとってはうまくいきます。

0
CallumVass

これらの例はすべて不完全でした。

以下は、使用後に接続を適切に閉じ、さらにこのスレッドの最新のより良い回答に基づいて、トランザクションスコープを正しく使用してExcecuteのパフォーマンスを向上させるコードです。

using (var scope = new TransactionScope()) 
{
    Connection.Open();
    Connection.Execute(sqlQuery, parameters);

    scope.Complete();
}
0
Erik Bergstedt