75 votes

Entity Framework 4.1. 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 des 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 qui existe entre ces deux requêtes, le cas échéant, une fois qu’elles ont été remplies. 'traduit'.

151voto

Slauma Points 76561

À l'aide de Contains dans l'Entité Cadre est effectivement très lent. C'est vrai que ça se traduit par un IN clause SQL et que la requête SQL lui-même est exécuté rapidement. Mais le problème et le goulot d'étranglement des performances est à la traduction à partir de votre requête LINQ en SQL. L'expression de l'arbre qui sera créé est développée en une longue chaîne de l' OR concaténations car il n'est pas natif de l'expression qui représente un IN. Lorsque le SQL est créée cette expression de nombreux ORs est reconnu et s'effondra en arrière dans le SQL IN de la clause.

Cela ne signifie pas que l'utilisation d' Contains est pire que la délivrance d'une requête par l'élément dans votre ids collection (votre première option). C'est probablement encore mieux - au moins pour ne pas trop grandes collections. Mais pour les grandes collections il est vraiment mauvais. Je me souviens que j'avais testé il y a quelques temps un Contains de la requête avec près de 12.000 éléments qui ont travaillé, mais a pris environ une minute, même si la requête en SQL exécuté en moins d'une seconde.

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

Cette approche et les limites de l'utilisation de Contains avec Entity Framework est montré et expliqué ici:

Pourquoi ne le Contient() l'opérateur de se dégrader Entité du Cadre de performance de façon spectaculaire?

Il est possible qu'un raw commande SQL donnent de meilleurs résultats dans cette situation, ce qui signifie que vous appelez dbContext.Database.SqlQuery<Image>(sqlString) ou dbContext.Images.SqlQuery(sqlString)sqlString est le SQL montré dans @Rune de réponse.

Modifier

Voici quelques mesures:

Je l'ai fait sur une table avec 550000 dossiers et 11 colonnes (Id 1 sans lacunes) et 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 -> ms = 85.5 sec

Test 2

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

Résultat -> ms = 84.5 sec

Ce petit effet de l' AsNoTracking est très inhabituel. Il indique que le goulot d'étranglement est pas l'objet de la réalisation (et non SQL, comme le montre ci-dessous).

Pour les deux tests, il peut être vu dans le générateur de profils SQL le SQL de la requête arrive à la base de données à très tard. (Je n'ai pas de mesurer exactement, mais il était plus tard que 70 secondes.) Évidemment, la traduction de cette requête LINQ en SQL est très cher.

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 -> ms = 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 -> ms = 3.8 sec

Cette fois, l'effet de la désactivation est plus perceptible.

Test 5

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

Résultat -> ms = 3,7 sec

Ma compréhension est qu' context.Database.SqlQuery<MyEntity>(sql) est le même que context.Set<MyEntity>().SqlQuery(sql).AsNoTracking(), donc il n'y a pas de différence attendue entre 4 et Test 5.

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

Edit 2

Test 6

Même 20000 allers-retours vers la base de données sont plus rapide 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 -> ms = 73.6 sec

Notez que j'ai utilisé SingleOrDefault au lieu de Find. En utilisant le même code avec Find est très lent (j'ai annulé le test au bout de plusieurs minutes), car Find des appels DetectChanges en interne. La désactivation automatique de la détection de changement (context.Configuration.AutoDetectChangesEnabled = false) conduit à peu près les mêmes performances que SingleOrDefault. À l'aide de AsNoTracking réduit le temps d'une ou deux secondes.

Des Tests ont été réalisés avec le client de base de données (application console) et le serveur de base de données sur la même machine. Le dernier résultat peut obtenir beaucoup plus de mal avec une "distance" de la base de données en raison des nombreux allers-retours.

4voto

Rune Points 4759

La deuxième option est nettement mieux que le premier. La première option ids.Length des requêtes à la base de données, tandis que la deuxième option peut utiliser un 'IN' de l'opérateur dans la requête SQL. Il va transformer votre requête LINQ en quelque chose comme le SQL suivant:

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

où valeur1, valeur2, etc. sont les valeurs de votre id de la variable. Soyez conscient, cependant, que je pense qu'il y a peut être une limite supérieure sur le nombre de valeurs qui peuvent être sérialisés dans une requête dans ce sens. Je vais voir si je peux trouver de la documentation...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X