MISE À JOUR 3: Selon cette annonce , l’équipe EF a répondu à cette question dans EF6 alpha 2.
MISE À JOUR 2: J’ai créé une suggestion pour résoudre ce problème. Pour voter, allez ici .
Considérons une firebase database SQL avec un tableau très simple.
CREATE TABLE Main (Id INT PRIMARY KEY)
Je remplis la table avec 10 000 enregistrements.
WITH Numbers AS ( SELECT 1 AS Id UNION ALL SELECT Id + 1 AS Id FROM Numbers WHERE Id <= 10000 ) INSERT Main (Id) SELECT Id FROM Numbers OPTION (MAXRECURSION 0)
Je construis un modèle EF pour la table et exécute la requête suivante dans LINQPad (j’utilise le mode “Instructions C #” pour que LINQPad ne crée pas de vidage automatiquement).
var rows = Main .ToArray();
Le temps d’exécution est d’environ 0.07 secondes. J’ajoute maintenant l’opérateur Contains et réexécute la requête.
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main .Where (a => ids.Contains(a.Id)) .ToArray();
Le temps d’exécution pour ce cas est de 20,14 secondes (288 fois plus lent)!
Au début, je soupçonnais que le T-SQL émis pour la requête prenait plus de temps à s’exécuter, alors j’ai essayé de le couper et de le coller depuis le volet SQL de LINQPad dans SQL Server Management Studio.
SET NOCOUNT ON SET STATISTICS TIME ON SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Primary] AS [Extent1] WHERE [Extent1].[Id] IN (1,2,3,4,5,6,7,8,...
Et le résultat était
SQL Server Execution Times: CPU time = 0 ms, elapsed time = 88 ms.
Ensuite, je soupçonnais que LINQPad était à l’origine du problème, mais les performances sont les mêmes que je l’exécute dans LINQPad ou dans une application console.
Il semble donc que le problème se situe quelque part dans Entity Framework.
Est-ce que je fais quelque chose de mal ici? C’est une partie critique de mon code, est-ce que je peux faire quelque chose pour accélérer les performances?
J’utilise Entity Framework 4.1 et Sql Server 2008 R2.
MISE À JOUR 1:
Dans la discussion ci-dessous, il y avait des questions sur le fait que le délai se produisait pendant que EF construisait la requête initiale ou pendant qu’il analysait les données reçues. Pour tester cela, j’ai exécuté le code suivant,
var ids = Main.Select(a => a.Id).ToArray(); var rows = (ObjectQuery) Main .Where (a => ids.Contains(a.Id)); var sql = rows.ToTraceSsortingng();
ce qui oblige EF à générer la requête sans l’exécuter sur la firebase database. Le résultat a été que ce code nécessitait environ 20 secords pour s’exécuter, il semble donc que presque tout le temps soit nécessaire pour créer la requête initiale.
ComstackdQuery à la rescousse alors? Pas si vite … ComstackdQuery nécessite que les parameters passés dans la requête soient des types fondamentaux (int, ssortingng, float, etc.). Il n’acceptera pas les tableaux ou IEnumerable, donc je ne peux pas l’utiliser pour une liste d’ID.
UPDATE: Avec l’ajout d’InExpression dans EF6, les performances de traitement de Enumerable.Contains se sont considérablement améliorées. L’approche décrite dans cette réponse n’est plus nécessaire.
Vous avez raison de dire que la majeure partie du temps est consacrée au traitement de la traduction de la requête. Le modèle de fournisseur EF ne comprend pas actuellement une expression représentant une clause IN, par conséquent, les fournisseurs ADO.NET ne peuvent pas prendre en charge IN en mode natif. Au lieu de cela, l’implémentation de Enumerable.Contains le traduit en une arborescence d’expressions OR, c’est-à-dire pour quelque chose qui dans C # ressemble à ceci:
new []{1, 2, 3, 4}.Contains(i)
… nous allons générer un arbre DbExpression qui pourrait être représenté comme ceci:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(Les arbres d’expression doivent être équilibrés, car si nous avions tous les blocages sur une longue colonne vertébrale, il y aurait plus de chances que le visiteur d’expression rencontre un débordement de stack (oui, nous l’avons effectivement fait dans nos tests))
Nous envoyons plus tard une arborescence comme celle-ci au fournisseur ADO.NET, qui peut reconnaître ce modèle et le réduire à la clause IN lors de la génération SQL.
Lorsque nous avons ajouté la prise en charge de Enumerable.Contains dans EF4, nous avons pensé qu’il était souhaitable de le faire sans avoir à prendre en charge les expressions IN dans le modèle fournisseur. Honnêtement, 10 000 dépasse largement le nombre d’éléments Enumerable.Contains. Cela dit, je comprends que c’est une gêne et que la manipulation d’arbres d’expressions rend les choses trop coûteuses dans votre scénario particulier.
J’en ai discuté avec l’un de nos développeurs et nous pensons qu’à l’avenir, nous pourrions changer d’implémentation en ajoutant un support de premier ordre pour IN. Je veillerai à ce que cela s’ajoute à notre arriéré, mais je ne peux pas vous promettre qu’il y aura beaucoup d’autres améliorations à apporter.
Aux solutions de contournement déjà suggérées dans le fil, j’appendais ce qui suit:
Envisagez de créer une méthode qui équilibre le nombre d’allers-retours avec la firebase database avec le nombre d’éléments que vous transmettez à Contains. Par exemple, dans mes propres tests, j’ai observé que le calcul et l’exécution sur une instance locale de SQL Server, avec une requête de 100 éléments, prenaient 1/60 de seconde. Si vous pouvez écrire votre requête de telle sorte que l’exécution de 100 requêtes avec 100 jeux d’identifiants différents vous donnerait un résultat équivalent à la requête avec 10 000 éléments, vous pouvez obtenir les résultats dans environ 1,67 secondes au lieu de 18 secondes.
Différentes tailles de blocs doivent mieux fonctionner en fonction de la requête et de la latence de la connexion à la firebase database. Pour certaines requêtes, par exemple si la séquence passée a des doublons ou si Enumerable.Contains est utilisé dans une condition nestede, vous pouvez obtenir des éléments en double dans les résultats.
Voici un extrait de code (désolé si le code utilisé pour découper les entrées en morceaux semble un peu trop complexe. Il existe des moyens plus simples pour réaliser la même chose, mais j’essayais de créer un modèle qui conserve le streaming pour la séquence et Je n’ai rien trouvé de pareil dans LINQ, alors j’ai probablement dépassé cette partie :)):
Usage:
var list = context.GetMainItems(ids).ToList();
Méthode de contexte ou de référentiel:
public partial class ContainsTestEntities { public IEnumerable GetMainItems(IEnumerable ids, int chunkSize = 100) { foreach (var chunk in ids.Chunk(chunkSize)) { var q = this.MainItems.Where(a => chunk.Contains(a.Id)); foreach (var item in q) { yield return item; } } } }
Méthodes d’extension pour découper des séquences énumérables:
public static class EnumerableSlicing { private class Status { public bool EndOfSequence; } private static IEnumerable TakeOnEnumerator (IEnumerator enumerator, int count, Status status) { while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true))) { yield return enumerator.Current; } } public static IEnumerable> Chunk (this IEnumerable items, int chunkSize) { if (chunkSize < 1) { throw new ArgumentException("Chunks should not be smaller than 1 element"); } var status = new Status { EndOfSequence = false }; using (var enumerator = items.GetEnumerator()) { while (!status.EndOfSequence) { yield return TakeOnEnumerator(enumerator, chunkSize, status); } } } }
J'espère que cela t'aides!
Si vous rencontrez un problème de performance qui bloque, n’essayez pas de le résoudre car vous ne réussirez probablement pas et vous devrez le communiquer directement avec MS (si vous avez un support premium). âge.
Utilisez une solution de contournement et une solution de contournement en cas de problème de performances et EF signifie SQL direct. Il n’y a rien de mal à cela. Idée globale selon laquelle utiliser EF = ne plus utiliser SQL est un mensonge. Vous disposez de SQL Server 2008 R2 pour:
SqlDataReader
pour obtenir des résultats et construire vos entités Si la performance est critique pour vous, vous ne trouverez pas de meilleure solution. Cette procédure ne peut pas être mappée et exécutée par EF car la version actuelle ne prend pas en charge les parameters de valeur de table ou les jeux de résultats multiples.
Nous avons été en mesure de résoudre le problème EF Contains en ajoutant une table intermédiaire et en joignant sur cette table à partir de la requête LINQ qui devait utiliser la clause Contains. Nous avons pu obtenir des résultats étonnants avec cette approche. Nous avons un grand modèle EF et comme “Contient” n’est pas autorisé lors de la pré-compilation des requêtes EF, nous obtenions de très mauvaises performances pour les requêtes utilisant la clause “Contains”.
Un aperçu:
Créez une table dans SQL Server – par exemple HelperForContainsOfIntType
avec HelperID
de HelperID
de données Guid
et ReferenceID
de colonnes de type de données int
. Créez des tables différentes avec ReferenceID de types de données différents selon vos besoins.
Créez un Entity / EntitySet pour HelperForContainsOfIntType
et d’autres tables similaires dans le modèle EF. Créez si nécessaire Entity / EntitySet pour différents types de données.
Créer une méthode d’assistance dans le code .NET qui prend l’entrée d’un IEnumerable
et renvoie un Guid
. Cette méthode génère un nouveau Guid
et insère les valeurs de IEnumerable
dans HelperForContainsOfIntType
avec le Guid
généré. Ensuite, la méthode renvoie ce Guid
nouvellement généré à l’appelant. Pour une insertion rapide dans la table HelperForContainsOfIntType
, créez une procédure stockée qui saisit une liste de valeurs et effectue l’insertion. Voir Paramètres de table dans SQL Server 2008 (ADO.NET) . Créez différents assistants pour différents types de données ou créez une méthode d’assistance générique pour gérer différents types de données.
Créez une requête EF compilée similaire à celle ci-dessous:
static Func> _selectCustomers = ComstackdQuery.Comstack( (MyEntities db, Guid containsHelperID) => from cust in db.Customers join x in db.HelperForContainsOfIntType on cust.CustomerID equals x.ReferenceID where x.HelperID == containsHelperID select cust );
Appelez la méthode d’assistance avec les valeurs à utiliser dans la clause Contains
et obtenez le Guid
à utiliser dans la requête. Par exemple:
var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 }); var result = _selectCustomers(_dbContext, containsHelperID).ToList();
Modification de ma réponse d’origine – Il existe une solution possible, en fonction de la complexité de vos entités. Si vous connaissez le fichier SQL généré par EF pour remplir vos entités, vous pouvez l’exécuter directement à l’aide de DbContext.Database.SqlQuery . Dans EF 4, je pense que vous pouvez utiliser ObjectContext.ExecuteStoreQuery , mais je ne l’ai pas essayé.
Par exemple, en utilisant le code de ma réponse d’origine ci-dessous pour générer l’instruction sql à l’aide de SsortingngBuilder
, j’ai pu effectuer les opérations suivantes:
var rows = db.Database.SqlQuery(sql).ToArray();
et le temps total est passé d’environ 26 secondes à 0,5 seconde.
Je serai le premier à dire que c’est moche, et j’espère qu’une meilleure solution se présentera.
Après un peu plus de reflection, j’ai réalisé que si vous utilisez une jointure pour filtrer vos résultats, EF n’a pas à créer cette longue liste d’identifiants. Cela peut être complexe en fonction du nombre de requêtes simultanées, mais je pense que vous pouvez utiliser des ID utilisateur ou des identifiants de session pour les isoler.
Pour tester cela, j’ai créé une table Target
avec le même schéma que Main
. J’ai ensuite utilisé un SsortingngBuilder
pour créer des commandes INSERT
afin de remplir la table Target
par lots de 1 000, car c’est la valeur que SQL Server acceptera dans un seul INSERT
. Exécuter directement les instructions SQL était beaucoup plus rapide que passer par EF (environ 0,3 seconde par rapport à 2,5 secondes), et je pense que ce serait correct car le schéma de la table ne devrait pas changer.
Enfin, la sélection en utilisant une join
abouti à une requête beaucoup plus simple et exécutée en moins de 0,5 seconde.
ExecuteStoreCommand("DELETE Target"); var ids = Main.Select(a => a.Id).ToArray(); var sb = new SsortingngBuilder(); for (int i = 0; i < 10; i++) { sb.Append("INSERT INTO Target(Id) VALUES ("); for (int j = 1; j <= 1000; j++) { if (j > 1) { sb.Append(",("); } sb.Append(i * 1000 + j); sb.Append(")"); } ExecuteStoreCommand(sb.ToSsortingng()); sb.Clear(); } var rows = (from m in Main join t in Target on m.Id equals t.Id select m).ToArray(); rows.Length.Dump();
Et le sql généré par EF pour la jointure:
SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] INNER JOIN [dbo].[Target] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
(réponse originale)
Ce n’est pas une réponse, mais je voulais partager des informations supplémentaires et il est beaucoup trop long de tenir compte d’un commentaire. J’ai pu reproduire vos résultats et append quelques autres éléments:
SQL Profiler indique que le délai est entre l’exécution de la première requête ( Main.Select
) et la seconde requête Main.Where
, donc je soupçonnais que le problème était dans la génération et l’envoi d’une requête de cette taille (48 980 octets).
Cependant, la construction dynamic de la même instruction sql dans T-SQL prend moins d’une seconde, et prendre les ids
de votre instruction Main.Select
, en construisant la même instruction sql et en l’exécutant avec SqlCommand
pris 0.112 secondes. le contenu à la console.
À ce stade, je soupçonne que EF effectue des parsings / traitements pour chacun des 10 000 ids
fur et à mesure qu’il construit la requête. Je voudrais pouvoir fournir une réponse définitive et une solution :(.
Voici le code que j’ai essayé dans SSMS et LINQPad (veuillez ne pas critiquer trop durement, je suis pressé d’essayer de quitter le travail):
declare @sql nvarchar(max) set @sql = 'SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN (' declare @count int = 0 while @count < 10000 begin if @count > 0 set @sql = @sql + ',' set @count = @count + 1 set @sql = @sql + cast(@count as nvarchar) end set @sql = @sql + ')' exec(@sql)
var ids = Mains.Select(a => a.Id).ToArray(); var sb = new SsortingngBuilder(); sb.Append("SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN ("); for(int i = 0; i < ids.Length; i++) { if (i > 0) sb.Append(","); sb.Append(ids[i].ToSsortingng()); } sb.Append(")"); using (SqlConnection connection = new SqlConnection("server = localhost;database = Test;integrated security = true")) using (SqlCommand command = connection.CreateCommand()) { command.CommandText = sb.ToSsortingng(); connection.Open(); using(SqlDataReader reader = command.ExecuteReader()) { while(reader.Read()) { Console.WriteLine(reader.GetInt32(0)); } } }
Je ne suis pas familier avec Entity Framework, mais est-ce que la performance est meilleure si vous faites ce qui suit?
Au lieu de cela:
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
que diriez-vous de cela (en supposant que l’ID est un int):
var ids = new HashSet(Main.Select(a => a.Id)); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
Il a été corrigé sur Entity Framework 6 Alpha 2: http://entityframework.codeplex.com/SourceControl/changeset/a7b70f69e551
http://blogs.msdn.com/b/adonet/archive/2012/12/10/ef6-alpha-2-available-on-nuget.aspx
Une alternative pouvant être mise en cache à Contains?
Cela m’a juste mordu donc j’ai ajouté mes deux pence au lien Entity Framework Feature Suggestions.
Le problème est certainement lors de la génération du SQL. J’ai un client sur qui les données la génération de la requête était de 4 secondes, mais l’exécution était de 0,1 seconde.
J’ai remarqué que lors de l’utilisation de LINQ et d’OR dynamics , la génération de sql prenait juste le temps mais elle générait quelque chose qui pourrait être mis en cache . Donc, lors de l’exécution à nouveau, il était tombé à 0,2 seconde.
Notez qu’un code SQL a toujours été généré.
Juste quelque chose d’autre à considérer si vous pouvez gérer le coup initial, votre nombre de tableaux ne change pas beaucoup, et lance beaucoup la requête. (Testé dans LINQ Pad)
Le problème concerne la génération SQL d’Entity Framework. Il ne peut pas mettre en cache la requête si l’un des parameters est une liste.
Pour que EF cache votre requête, vous pouvez convertir votre liste en chaîne et créer un fichier .Contains sur la chaîne.
Ainsi, par exemple, ce code s’exécuterait beaucoup plus rapidement car EF pourrait mettre en cache la requête:
var ids = Main.Select(a => a.Id).ToArray(); var idsSsortingng = "|" + Ssortingng.Join("|", ids) + "|"; var rows = Main .Where (a => idsSsortingng.Contains("|" + a.Id + "|")) .ToArray();
Lorsque cette requête est générée, elle sera probablement générée avec un Like au lieu d’un In, ce qui accélérera votre C # mais risque de ralentir votre SQL. Dans mon cas, je n’ai remarqué aucune baisse de performance dans mon exécution SQL, et le C # s’est exécuté beaucoup plus rapidement.