Question Le moyen le plus rapide d'insertion dans Entity Framework


Je suis à la recherche du moyen le plus rapide d'insérer dans Entity Framework.

Je demande cela à cause du scénario où vous avez un TransactionScope actif et l'insertion est énorme (4000+). Il peut potentiellement durer plus de 10 minutes (délai d'expiration par défaut des transactions), ce qui entraînera une transaction incomplète.


543
2018-05-09 17:14


origine


Réponses:


À votre remarque dans les commentaires à votre question:

"...Enregistrer les modifications (pour chaque   record) ... "

C'est la pire chose que tu puisses faire! Appel SaveChanges() pour chaque enregistrement ralentit insère en vrac extrêmement bas. Je ferais quelques tests simples qui vont très probablement améliorer les performances:

  • Appel SaveChanges() une fois après TOUS les enregistrements.
  • Appel SaveChanges() après par exemple 100 enregistrements.
  • Appel SaveChanges() après par exemple 100 enregistrements et disposer du contexte et en créer un nouveau.
  • Désactiver la détection de changement

Pour les encarts en vrac, je travaille et expérimente avec un motif comme celui-ci:

using (TransactionScope scope = new TransactionScope())
{
    MyDbContext context = null;
    try
    {
        context = new MyDbContext();
        context.Configuration.AutoDetectChangesEnabled = false;

        int count = 0;            
        foreach (var entityToInsert in someCollectionOfEntitiesToInsert)
        {
            ++count;
            context = AddToContext(context, entityToInsert, count, 100, true);
        }

        context.SaveChanges();
    }
    finally
    {
        if (context != null)
            context.Dispose();
    }

    scope.Complete();
}

private MyDbContext AddToContext(MyDbContext context,
    Entity entity, int count, int commitCount, bool recreateContext)
{
    context.Set<Entity>().Add(entity);

    if (count % commitCount == 0)
    {
        context.SaveChanges();
        if (recreateContext)
        {
            context.Dispose();
            context = new MyDbContext();
            context.Configuration.AutoDetectChangesEnabled = false;
        }
    }

    return context;
}

J'ai un programme de test qui insère 560.000 entités (9 propriétés scalaires, pas de propriétés de navigation) dans la base de données. Avec ce code, il fonctionne en moins de 3 minutes.

Pour la performance, il est important d'appeler SaveChanges() après "plusieurs" enregistrements ("beaucoup" autour de 100 ou 1000). Il améliore également les performances pour disposer du contexte après SaveChanges et en créer un nouveau. Cela efface le contexte de tous les entites, SaveChanges ne fait pas cela, les entités sont toujours attachées au contexte dans l'état Unchanged. C'est la taille croissante des entités attachées dans le contexte qui ralentit l'insertion étape par étape. Donc, il est utile de l'effacer après un certain temps.

Voici quelques mesures pour mes 560 000 entités:

  • commitCount = 1, recreateContext = false: beaucoup d'heures (C'est votre procédure actuelle)
  • commitCount = 100, recreateContext = false: plus de 20 minutes
  • commitCount = 1000, recreateContext = false: 242 sec
  • commitCount = 10000, recreateContext = false: 202 sec
  • commitCount = 100000, recreateContext = false: 199 sec
  • commitCount = 1000000, recreateContext = false: Exception de mémoire insuffisante
  • commitCount = 1, recreateContext = true: plus de 10 minutes
  • commitCount = 10, recreateContext = true: 241 sec
  • commitCount = 100, recreateContext = true: 164 sec
  • commitCount = 1000, recreateContext = true: 191 sec

Le comportement dans le premier test ci-dessus est que la performance est très non linéaire et diminue extrêmement au fil du temps. ("Beaucoup d'heures" est une estimation, je n'ai jamais fini ce test, je me suis arrêté à 50.000 entités après 20 minutes.) Ce comportement non linéaire n'est pas si important dans tous les autres tests.


841
2018-05-09 20:33



Cette combinaison augmente la vitesse assez bien.

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;

162
2017-08-12 15:52



Le moyen le plus rapide serait d'utiliser extension d'insert en vrac, que j'ai développé.

Il utilise SqlBulkCopy et le lecteur de données personnalisé pour obtenir des performances maximales. En conséquence, il est plus de 20 fois plus rapide que l'insertion régulière ou AddRange EntityFramework.BulkInsert vs EF AddRange

l'utilisation est extrêmement simple

context.BulkInsert(hugeAmountOfEntities);

96
2018-02-17 21:18



Vous devriez regarder en utilisant le System.Data.SqlClient.SqlBulkCopy pour ça. Ici se trouve le Documentation, et bien sûr il y a beaucoup de tutoriels en ligne.

Désolé, je sais que vous cherchiez une réponse simple pour qu'EF fasse ce que vous voulez, mais les opérations en vrac ne sont pas vraiment ce que les ORM sont destinés.


66
2018-05-09 17:17



Je suis d'accord avec Adam Rackis. SqlBulkCopy est le moyen le plus rapide de transférer des enregistrements en masse d'une source de données à une autre. J'ai utilisé ceci pour copier des enregistrements 20K et cela a pris moins de 3 secondes. Jetez un oeil à l'exemple ci-dessous.

public static void InsertIntoMembers(DataTable dataTable)
{           
    using (var connection = new SqlConnection(@"data source=;persist security info=True;user id=;password=;initial catalog=;MultipleActiveResultSets=True;App=EntityFramework"))
    {
        SqlTransaction transaction = null;
        connection.Open();
        try
        {
            transaction = connection.BeginTransaction();
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = "Members";
                sqlBulkCopy.ColumnMappings.Add("Firstname", "Firstname");
                sqlBulkCopy.ColumnMappings.Add("Lastname", "Lastname");
                sqlBulkCopy.ColumnMappings.Add("DOB", "DOB");
                sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
                sqlBulkCopy.ColumnMappings.Add("Email", "Email");

                sqlBulkCopy.ColumnMappings.Add("Address1", "Address1");
                sqlBulkCopy.ColumnMappings.Add("Address2", "Address2");
                sqlBulkCopy.ColumnMappings.Add("Address3", "Address3");
                sqlBulkCopy.ColumnMappings.Add("Address4", "Address4");
                sqlBulkCopy.ColumnMappings.Add("Postcode", "Postcode");

                sqlBulkCopy.ColumnMappings.Add("MobileNumber", "MobileNumber");
                sqlBulkCopy.ColumnMappings.Add("TelephoneNumber", "TelephoneNumber");

                sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted");

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
        }

    }
}

46
2018-01-15 10:27



J'ai étudié la réponse de Slauma (ce qui est génial, merci pour l'idée de l'homme), et j'ai réduit la taille du lot jusqu'à ce que j'aie atteint la vitesse optimale. En regardant les résultats du Slauma:

  • commitCount = 1, recreateContext = true: plus de 10 minutes
  • commitCount = 10, recreateContext = true: 241 sec
  • commitCount = 100, recreateContext = true: 164 sec
  • commitCount = 1000, recréerContext = true: 191 sec

Il est visible qu'il y a une augmentation de la vitesse en passant de 1 à 10, et de 10 à 100, mais de 100 à 1000, la vitesse d'insertion redescend.

Je me suis donc concentré sur ce qui se passe lorsque vous réduisez la taille d'un lot à une valeur comprise entre 10 et 100, et voici mes résultats (j'utilise un contenu de ligne différent, mes temps ont donc une valeur différente):

Quantity    | Batch size    | Interval
1000    1   3
10000   1   34
100000  1   368

1000    5   1
10000   5   12
100000  5   133

1000    10  1
10000   10  11
100000  10  101

1000    20  1
10000   20  9
100000  20  92

1000    27  0
10000   27  9
100000  27  92

1000    30  0
10000   30  9
100000  30  92

1000    35  1
10000   35  9
100000  35  94

1000    50  1
10000   50  10
100000  50  106

1000    100 1
10000   100 14
100000  100 141

Sur la base de mes résultats, l'optimum réel est de l'ordre de 30 pour la taille du lot. C'est moins que les deux 10 et 100. Le problème est, je ne sais pas pourquoi 30 est optimal, et je n'ai pu trouver aucune explication logique pour cela.


18
2018-03-26 08:46



Je recommanderais cet article sur comment faire des insertions en vrac en utilisant EF.

Entity Framework et INSERT en masse lente

Il explore ces domaines et compare la performance:

  1. EF par défaut (57 minutes pour compléter l'ajout de 30 000 enregistrements)
  2. Remplacement par le code ADO.NET (25 secondes pour ces mêmes 30 000)
  3. Context Bloat - Gardez le graphique de contexte actif petit en utilisant un nouveau contexte pour chaque unité de travail (même 30 000 insertions prennent 33 secondes)
  4. Grandes listes - Désactivez AutoDetectChangesEnabled (réduit le temps à environ 20 secondes)
  5. Lot (jusqu'à 16 secondes)
  6. DbTable.AddRange () - (la performance est dans la gamme 12)

16
2018-06-20 03:10



Comme d'autres personnes l'ont dit, SqlBulkCopy est le moyen de le faire si vous voulez vraiment de bonnes performances d'insertion.

C'est un peu lourd à mettre en place, mais il y a des bibliothèques qui peuvent vous aider. Il y en a quelques-uns mais je vais sans scrupule brancher ma propre bibliothèque cette fois-ci: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

Le seul code dont vous auriez besoin est:

 using (var db = new YourDbContext())
 {
     EFBatchOperation.For(db, db.BlogPosts).InsertAll(list);
 }

Alors, combien plus vite est-ce? Très difficile à dire car cela dépend de nombreux facteurs, des performances de l'ordinateur, du réseau, de la taille de l'objet, etc. Les tests de performance que j'ai effectués suggèrent que 25k entités peuvent être insérées à environ 10s le manière standard sur localhost SI vous optimisez votre configuration EF comme mentionné dans les autres réponses. Avec EFUtilities qui prend environ 300ms. Encore plus intéressant est que j'ai économisé environ 3 millions d'entités en moins de 15 secondes en utilisant cette méthode, avec une moyenne d'environ 200 000 entités par seconde.

Le problème est bien sûr si vous avez besoin d'insérer des données releated. Cela peut être fait efficacement dans le serveur SQL en utilisant la méthode ci-dessus, mais cela nécessite que vous ayez une stratégie de génération d'ID qui vous permet de générer des ID dans le code de l'application pour le parent afin de pouvoir définir les clés étrangères. Cela peut être fait en utilisant des GUID ou quelque chose comme la génération d'ID HiLo.


14
2017-12-19 10:44