使用Dapper进行批量插入所花费的时间比预期的长


69

阅读本文后,我决定仔细研究一下我使用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次插入。不错,但考虑到博客速度达到每秒45,000次插入,效果也不佳。Dapper中有更有效的方法吗?

另外,请注意,通过Visual Studio调试器运行此代码需要3分钟以上!我认为调试器会使它放慢一点,但是看到这么多,我真的很惊讶。

更新

所以这

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();

3
您是否尝试过DbTransaction(SqlTransaction)?这样可以减少开销。此外,仅用于数字:也许也无需交易即可尝试,因此我们知道我们要衡量的内容。最后,20多岁跨度如何?插入?插入内容是否完整?一切?还有吗
马克·Gravell

20s只是using语句包含的部分。我将尝试SqlTransaction
kenwarner,2012年

1
TransactionScope会执行许多您通常不关心的DTC废话,除非我需要该功能,否则将避免使用它,将自己的上下文附加到线程本地存储非常容易
Sam Saffron 2012年

1
@MarcGravell我认为他正在使用事务,只是没有使用完成所有dtc废话的transactionscope
Sam Saffron 2012年

2
@MarcGravell注意,甚至SqlTransaction也比“ begin tran”有一些额外的开销
Sam Saffron

Answers:


79

使用此方法,我能够在4秒内达到的最高记录是5万条记录

SqlTransaction trans = connection.BeginTransaction();

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

trans.Commit();

1
也许该连接未加入事务中,而您之前遇到了问题?
GorillaApe 2012年

11
@Parhs:改善了50%。</ pedantic>
skolima

1
@skolima虽然,您可以将其从“每分钟3条记录”改为“每分钟6条记录”(或任何等量的量),但它却是100%的改进
Rob

3
伙计们-我只是看着sql server profiler而感到困惑。此代码不进行批处理(至少对我而言)-每次操作插入一行(表示1500行-1500个单独的插入命令)。我将Sql Server 2012与最新的dapper一起使用
chester89

2
@ chester89我同意你的看法。这不是批量插入任何内容。这只是常规插入。我认为目前精巧的布料无法处理大量插入物
乔·菲利普斯

13

我最近偶然发现了这个问题,并注意到在打开连接后创建了TransactionScope(我之所以这样认为是因为Dappers Execute不像Query那样打开连接)。根据此处的答案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();
}

4
如果尝试这样做ExecuteAsync,将引发异常:“必须将TransactionScope放在创建它的同一线程上”。为避免这种情况:using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
Josh Noe

3

我创建了一个扩展方法,使您可以非常快速地进行批量插入。

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

我发现所有这些示例都不完整。

这是一些代码,该代码在使用后可以正确关闭连接,并根据该线程的最新更新和更好的答案正确使用transactionscope来增强Excecute性能。

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

    scope.Complete();
}

using关键字自动调用IDispose,后者调用.Close()。using关键字将您的代码在编译时转换为try / finally,在最后一次调用.Dispose()。
Cubicle.Jockey,2016年

我以为IDispose只叫Dispose?
Erik Bergstedt'6

你是对的。它调用Dispose,但是Dispose的连接实现调用Close()。我还假设这是一个SqlConnection对象。但是,是的,使用有帮助,因此您不要忘记像在Dispose中那样调用close。
Cubicle.Jockey,2016年

0

Execute仅使用一个insert语句使用该方法将永远不会进行批量插入或效率很高。即使使用a接受的答案Transaction也不会执行Bulk Insert

如果要执行Bulk Insert,请使用SqlBulkCopy https://msdn.microsoft.com/zh-cn/library/system.data.sqlclient.sqlbulkcopy

您将找不到比这更快的东西。

Dapper Plus

免责声明:我是Dapper Plus项目的所有者

该项目不是免费的,但提供所有批量操作:

  • 批量插入
  • 批量更新
  • 批量删除
  • 批量合并

(在引擎盖下使用SqlBulkCopy

还有其他一些选项,例如输出标识值:

// 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的
  • 火鸟

-4

对我来说最快的变体:

            var dynamicParameters = new DynamicParameters();
            var selects = new List<string>();
            for (var i = 0; i < members.Length; i++)
            {
                var member = members[i];
                var pUsername = $"u{i}";
                var pIsActive = $"a{i}";
                dynamicParameters.Add(pUsername, member.Username);
                dynamicParameters.Add(pIsActive, member.IsActive);
                selects.Add("select @{pUsername},@{pIsActive}");
            }
            con.Execute($"insert into Member(Username, IsActive){string.Join(" union all ", selects)}", dynamicParameters);

生成像这样的sql:

INSERT TABLENAME (Column1,Column2,...)
 SELECT @u0,@a0...
 UNION ALL
 SELECT @u1,@a1...
 UNION ALL
 SELECT @u2,@a2...

此查询的运行速度更快,因为sql会添加行集,而不是一次添加1行。瓶颈不是写数据,而是写您在日志中正在做的事情。

另外,请查看最少记录事务的规则。


@ m0sa,我已将其修复:)
razon
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.