web-dev-qa-db-fra.com

Pourquoi .Contains est lent? Le moyen le plus efficace d’obtenir plusieurs entités par clé primaire?

Quel est le moyen le plus efficace de sélectionner plusieurs entités par clé primaire?

public IEnumerable<Models.Image> GetImagesById(IEnumerable<int> ids)
{

    //return ids.Select(id => Images.Find(id));       //is this cool?
    return Images.Where( im => ids.Contains(im.Id));  //is this better, worse or the same?
    //is there a (better) third way?

}

Je me rends compte que je pourrais faire quelques tests de performance pour comparer, mais je me demande s’il existe en fait un meilleur moyen que les deux, et je cherche des éclaircissements sur la différence entre ces deux requêtes, le cas échéant, une fois qu’elles ont été traitées 'traduit'.

54
Tom

UPDATE: Avec l'ajout d'InExpression dans EF6, les performances du traitement Enumerable.Contains ont été considérablement améliorées. L'analyse présentée dans cette réponse est excellente mais largement obsolète depuis 2013.

L'utilisation de Contains dans Entity Framework est en réalité très lente. Il est vrai que cela se traduit par une clause IN en SQL et que la requête SQL elle-même est exécutée rapidement. Mais le problème et le goulot d'étranglement des performances réside dans la traduction de votre requête LINQ en SQL. L'arbre d'expression qui sera créé est développé dans une longue chaîne de concaténations OR car il n'y a pas d'expression native représentant un IN. Lors de la création du code SQL, cette expression de plusieurs ORs est reconnue et repliée dans la clause SQL IN.

Cela ne signifie pas que l'utilisation de Contains est pire que d'émettre une requête par élément dans votre collection ids (votre première option). C'est probablement encore mieux - du moins pour les collections pas trop grandes. Mais pour les grandes collections, c'est vraiment mauvais. Je me souviens que j’avais testé il ya quelque temps une requête Contains avec environ 12 000 éléments qui fonctionnait mais prenait environ une minute bien que la requête SQL s’exécute en moins d’une seconde.

Il peut être intéressant de tester les performances d'une combinaison de plusieurs allers-retours dans la base de données avec un plus petit nombre d'éléments dans une expression Contains pour chaque aller-retour.

Cette approche, ainsi que les limites d'utilisation de Contains avec Entity Framework, sont expliquées et expliquées ici:

Pourquoi l'opérateur Contains () dégrade-t-il les performances d'Entity Framework de manière aussi spectaculaire?

Il est possible qu'une commande SQL brute fonctionne mieux dans cette situation, ce qui signifie que vous appelez dbContext.Database.SqlQuery<Image>(sqlString) ou dbContext.Images.SqlQuery(sqlString)sqlString est le code SQL indiqué dans la réponse de @ Rune.

Modifier

Voici quelques mesures:

Je l'ai fait sur une table avec 550000 enregistrements et 11 colonnes (les ID commencent à 1 sans espaces) et j'ai choisi au hasard 20000 ID:

using (var context = new MyDbContext())
{
    Random Rand = new Random();
    var ids = new List<int>();
    for (int i = 0; i < 20000; i++)
        ids.Add(Rand.Next(550000));

    Stopwatch watch = new Stopwatch();
    watch.Start();

    // here are the code snippets from below

    watch.Stop();
    var msec = watch.ElapsedMilliseconds;
}

Test 1

var result = context.Set<MyEntity>()
    .Where(e => ids.Contains(e.ID))
    .ToList();

Résultat -> msec = 85.5 sec

Test 2

var result = context.Set<MyEntity>().AsNoTracking()
    .Where(e => ids.Contains(e.ID))
    .ToList();

Résultat -> msec = 84.5 sec

Cet effet minuscule de AsNoTracking est très inhabituel. Cela indique que le goulot d'étranglement n'est pas une matérialisation d'objet (et non pas SQL, comme indiqué ci-dessous).

SQL Profiler permet de constater que la requête SQL parvient très tard à la base de données. (Je n'ai pas mesuré exactement mais cela a pris plus de 70 secondes.) Évidemment, la traduction de cette requête LINQ en SQL est très coûteuse.

Test 3

var values = new StringBuilder();
values.AppendFormat("{0}", ids[0]);
for (int i = 1; i < ids.Count; i++)
    values.AppendFormat(", {0}", ids[i]);

var sql = string.Format(
    "SELECT * FROM [MyDb].[dbo].[MyEntities] WHERE [ID] IN ({0})",
    values);

var result = context.Set<MyEntity>().SqlQuery(sql).ToList();

Résultat -> msec = 5.1 sec

Test 4

// same as Test 3 but this time including AsNoTracking
var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList();

Résultat -> msec = 3.8 sec

Cette fois, l’effet de désactiver le suivi est plus visible.

Test 5

// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();

Résultat -> msec = 3.7 sec

D'après ce que je comprends, context.Database.SqlQuery<MyEntity>(sql) est identique à context.Set<MyEntity>().SqlQuery(sql).AsNoTracking(); il n'y a donc pas de différence attendue entre le test 4 et le test 5.

(La longueur des ensembles de résultats n'était pas toujours la même en raison des doublons possibles après la sélection id aléatoire, mais elle était toujours comprise entre 19600 et 19640.)

Edit 2

Test 6

Même 20000 allers-retours à la base de données sont plus rapides que d'utiliser Contains:

var result = new List<MyEntity>();
foreach (var id in ids)
    result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id));

Résultat -> msec = 73.6 sec

Notez que j'ai utilisé SingleOrDefault au lieu de Find. L'utilisation du même code avec Find est très lente (j'ai annulé le test après plusieurs minutes) car Find appelle DetectChanges en interne. La désactivation de la détection de changement automatique (context.Configuration.AutoDetectChangesEnabled = false) entraîne à peu près les mêmes performances que SingleOrDefault. Utiliser AsNoTracking réduit le temps d'une ou deux secondes.

Les tests ont été effectués avec le client de base de données (application console) et le serveur de base de données sur le même ordinateur. Le dernier résultat pourrait être considérablement pire avec une base de données "distante" en raison des nombreux allers-retours.

127
Slauma

La deuxième option est définitivement meilleure que la première. La première option entraînera des requêtes ids.Length dans la base de données, tandis que la seconde option peut utiliser un opérateur 'IN' dans la requête SQL. Cela transformera votre requête LINQ en quelque chose comme le code SQL suivant:

SELECT *
FROM ImagesTable
WHERE id IN (value1,value2,...)

où valeur1, valeur2 etc. sont les valeurs de votre variable ids. Sachez cependant que, selon moi, le nombre de valeurs pouvant être sérialisées dans une requête peut être limité de cette manière. Je verrai si je peux trouver de la documentation ...

4
Rune

Nous avons récemment rencontré un problème similaire. Le meilleur moyen que j’ai trouvé est d’insérer la liste des éléments contenus dans une table temporaire et de créer une jointure.

private List<Foo> GetFoos(IEnumerable<long> ids)
{
    var sb = new StringBuilder();
    sb.Append("DECLARE @Temp TABLE (Id bitint PRIMARY KEY)\n");

    foreach (var id in ids)
    {
        sb.Append("INSERT INTO @Temp VALUES ('");
        sb.Append(id);
        sb.Append("')\n");
    }

    sb.Append("SELECT f.* FROM [dbo].[Foo] f inner join @Temp t on f.Id = t.Id");

    return this.context.Database.SqlQuery<Foo>(sb.ToString()).ToList();
}

Ce n'est pas beau, mais pour les grandes listes, c'est très performant.

0
nelson eldoro

Transformer la liste en tableau avec toArray () augmente les performances. Vous pouvez le faire de cette façon:

ids.Select(id => Images.Find(id));     
    return Images.toArray().Where( im => ids.Contains(im.Id));  
0

J'utilise Entity Framework 6.1 et découvre en utilisant votre code que, il est préférable d'utiliser:

return db.PERSON.Find(id);

plutôt que:

return db.PERSONA.FirstOrDefault(x => x.ID == id);

Performances de Find () par rapport à FirstOrDefault sont quelques réflexions à ce sujet.

0
Juanito