web-dev-qa-db-fra.com

Les insertions en masse prennent plus de temps que prévu à l'aide de Dapper

Après avoir lu cet article j'ai décidé de regarder de plus près la façon dont j'utilisais Dapper.

J'ai exécuté ce code sur une base de données vide

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

cela a pris environ 20 secondes. C'est 2500 inserts/seconde. Pas mal, mais pas super non plus étant donné que le blog atteignait 45 000 insertions/seconde. Existe-t-il un moyen plus efficace de le faire dans Dapper?

En outre, en guise de remarque, l'exécution de ce code via le débogueur Visual Studio a pris plus de 3 minutes! J'ai pensé que le débogueur le ralentirait un peu, mais J'étais vraiment surpris de voir ça.

[~ # ~] mise à jour [~ # ~]

Donc ça

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

    scope.Complete();
}

et ça

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

les deux ont pris 20 secondes.

Mais cela a pris 4 secondes!

SqlTransaction trans = connection.BeginTransaction();

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

trans.Commit();
63
kenwarner

Le meilleur que j'ai pu atteindre était de 50 000 enregistrements en 4 secondes en utilisant cette approche

SqlTransaction trans = connection.BeginTransaction();

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

trans.Commit();
70
kenwarner

Je suis tombé sur cela récemment et j'ai remarqué que TransactionScope est créé après l'ouverture de la connexion (je suppose que puisque Dappers Execute n'ouvre pas la connexion, contrairement à Query). Selon la réponse Q4 ici: https://stackoverflow.com/a/2886326/455904 qui n'entraînera pas la connexion à gérer par le TransactionScope. Mon collègue a effectué des tests rapides et l'ouverture de la connexion en dehors de TransactionScope a considérablement réduit les performances.

Donc, changer pour ce qui suit devrait fonctionner:

// 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

L'utilisation de la méthode Execute avec une seule instruction d'insertion ne fera jamais une insertion en bloc ni ne sera efficace. Même la réponse acceptée avec un Transaction ne fait pas un Bulk Insert.

Si vous souhaitez effectuer un Bulk Insert, utilisez le SqlBulkCopyhttps://msdn.Microsoft.com/en-us/library/system.data.sqlclient.sqlbulkcopy

Vous ne trouverez rien de plus rapide que cela.

Dapper Plus

Avertissement : Je suis le propriétaire du projet Dapper Plus

Ce projet n'est pas gratuit mais propose toutes les opérations en masse:

  • BulkInsert
  • BulkUpdate
  • BulkDelete
  • BulkMerge

(Utiliser sous le capot SqlBulkCopy)

Et quelques autres options telles que la sortie des valeurs d'identité:

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

Notre bibliothèque prend en charge plusieurs fournisseurs:

  • Serveur SQL
  • SQL Compact
  • Oracle
  • MySql
  • PostgreSQL
  • SQLite
  • Oiseau de feu
0
Jonathan Magnan

J'ai créé une méthode d'extension qui vous permettrait de faire un insert en masse très rapidement.

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;
    }
}

Ensuite, pour utiliser cette méthode d'extension, vous écririez du code comme suit:

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

C'est assez primitif et peut encore être amélioré, comme passer une transaction ou une valeur commandTimeout, mais ça fait l'affaire pour moi.

0
CallumVass

J'ai trouvé tous ces exemples incomplets.

Voici du code qui ferme correctement la connexion après utilisation et utilise également correctement le transactionscope pour améliorer les performances d'Excute, en fonction des réponses les plus récentes et les meilleures de ce fil.

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

    scope.Complete();
}
0
Erik Bergstedt