Il semble qu'il y ait une fausse idée commune sur ce que LINQ GroupBy
et ce que fait SQL GROUP BY
est capable de faire. Étant donné que je suis tombé dans le même piège et que j'ai dû me faire une raison récemment, j'ai décidé d'écrire une explication plus approfondie de cette question.
Réponse courte :
Le LINQ GroupBy
est très différent à partir du SQL GROUP BY
déclaration : LINQ juste divise la collection sous-jacente en morceaux en fonction d'une clé, tandis que le SQL ajoute applique une fonction d'agrégation pour condenser chacun de ces morceaux dans un valeur unique .
C'est pourquoi EF doit effectuer votre LINQ-kind GroupBy
en mémoire.
Avant EF Core 3.0, on procédait ainsi implicitement EF a donc téléchargé toutes les lignes de résultats et a ensuite appliqué la méthode LINQ GroupBy
. Cependant, ce comportement implicite pourrait laisser le programmeur s'attendre à ce que l'élément tout le site Les requêtes LINQ sont exécutées en SQL, avec un impact potentiellement énorme sur les performances lorsque l'ensemble de résultats est assez grand. Pour cette raison, l'évaluation implicite côté client de la requête GroupBy
était désactivé complètement dans EF Core 3.0 .
Il est maintenant nécessaire d'appeler explicitement des fonctions telles que .AsEnumerable()
o .ToList()
qui téléchargent l'ensemble des résultats et poursuivent les opérations LINQ en mémoire.
Longue réponse :
Le tableau suivant solvedExercises
sera l'exemple courant pour cette réponse :
+-----------+------------+
| StudentId | ExerciseId |
+-----------+------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
+-----------+------------+
Un dossier X | Y
dans ce tableau indique que l'étudiant X
a résolu l'exercice Y
.
Dans la question, un cas courant d'utilisation de la fonction LINQ GroupBy
est décrite : Prenez une collection et regroupez-la en chunks, où les lignes de chaque chunk partagent une clé commune.
Dans notre exemple, nous pourrions vouloir obtenir une Dictionary<int, List<int>>
qui contient une liste d'exercices résolus pour chaque étudiant. Avec LINQ, c'est très simple :
var result = solvedExercises
.GroupBy(e => e.StudentId)
.ToDictionary(e => e.Key, e => e.Select(e2 => e2.ExerciseId).ToList());
Sortie (pour le code complet, voir dotnetfiddle ) :
Student #1: 1 2
Student #2: 2
Student #3: 1 2 3
Ceci est facile à représenter avec les types de données C#, puisque nous pouvons imbriquer les éléments suivants List
y Dictionary
aussi profond que nous le souhaitons.
Essayons maintenant de l'imaginer comme un résultat de requête SQL. Les résultats des requêtes SQL sont généralement représentés sous la forme d'un tableau, dans lequel nous pouvons choisir librement les colonnes renvoyées. Pour représenter notre requête ci-dessus en tant que résultat de requête SQL, nous devrions
- générer plusieurs tableaux de résultats,
- mettre les lignes groupées dans un tableau ou
- d'une manière ou d'une autre, insérer un "séparateur d'ensembles de résultats".
Pour autant que je sache, aucune de ces approches n'est mise en œuvre dans la pratique. Tout au plus, il existe des solutions de fortune comme la méthode de MySQL GROUP_CONCAT
qui permet de combiner les lignes de résultats en une chaîne de caractères ( réponse SO pertinente ).
Ainsi nous voyons, que SQL ne peut pas donnent des résultats qui correspondent à la notion de LINQ de GroupBy
.
Au lieu de cela, SQL ne permet que les soi-disant agrégation : Si nous voulons, par exemple, compter combien d'exercices ont été réussis par un élève, nous écrirons
SELECT StudentId,COUNT(ExerciseId)
FROM solvedExercises
GROUP BY StudentId
...ce qui donnera
+-----------+-------------------+
| StudentId | COUNT(ExerciseId) |
+-----------+-------------------+
| 1 | 2 |
| 2 | 1 |
| 3 | 3 |
+-----------+-------------------+
Les fonctions d'agrégation réduisent un ensemble de lignes en une seule valeur, généralement un scalaire. Les exemples sont le nombre de lignes, la somme, la valeur maximale, la valeur minimale et la moyenne.
Ce site es mis en œuvre par EF Core : Exécution de
var result = solvedExercises
.GroupBy(e => e.StudentId)
.Select(e => new { e.Key, Count = e.Count() })
.ToDictionary(e => e.Key, e => e.Count);
génère le SQL ci-dessus. Notez le Select
qui indique à EF quel fonction d'agrégation qu'il doit utiliser pour la requête SQL générée.
En résumé, le système LINQ GroupBy
est beaucoup plus générale que la fonction SQL GROUP BY
qui, en raison des restrictions de SQL, ne permet de renvoyer qu'un seul tableau de résultats à deux dimensions. Ainsi, les requêtes comme celle de la question et le premier exemple de cette réponse doivent être évaluées en mémoire, après avoir téléchargé le jeu de résultats SQL.
Au lieu de implicitement Pour ce faire, dans EF Core 3.0 les développeurs a choisi de lancer une exception dans ce cas ; cela permet d'éviter le téléchargement accidentel d'une table entière, potentiellement importante, comportant des millions de lignes, qui pourrait passer inaperçue pendant le développement en raison d'une petite base de données de test.
0 votes
Je suppose que GroupBy s'est trompé. Même si vous n'écrivez que GroupBy dans la requête, vous obtenez la même erreur. Ma seule solution était également d'utiliser AsEnumerable() avant GroupBy().
0 votes
J'ai le même problème et j'ai rétrogradé vers dotnetcore 2.2 et .NetStandard 2.0 pour continuer à travailler. Il n'est pas logique de bloquer une fonctionnalité qui fonctionne. D'accord, cela pénalise les performances, mais je le sais et j'en ai besoin.
0 votes
@VanoMaisuradze Je pense que dans EF Core 3.0 il est toujours nécessaire d'utiliser une fonction comme MAX, AVG, ... avant le GroupBy. J'essaie de comprendre quelle est la meilleure façon de résoudre ce problème... En général, en SQL, il suffit de SELECTIONNER la colonne qui est utilisée dans le GroupBy pour que cela fonctionne ...
4 votes
@Duefectu Utiliser NET Core 2.2 ou utiliser NET Core 3.0 avec .AsEnumerable() revient au même ... Les deux s'exécutent sur le client. Il n'est donc pas nécessaire de passer à la version 2.2. Il suffit d'utiliser .AsEnumerable(). Ma question est de savoir comment ne pas utiliser .AsEnumerable() dans ma requête pour que tout soit exécuté sur le serveur.
0 votes
En dehors de votre question, le problème est de savoir comment porter un gros projet vers dotnetcore 3.0 sans changer la moitié de mes requêtes linq.
1 votes
La seule solution de contournement semble être jusqu'à présent d'utiliser
.AsEnumerable()
o.ToList()
avantGroupBy
pour contourner les bogues du traducteur de requêtes ef core. Vous pouvez utiliserWhere
pour récupérer le moins de données possible.3 votes
Si vous trouvez des bogues ou si vous avez des questions concernant l'implémentation de EF 3, veuillez d'abord vérifier les points suivants aquí s'il s'agit d'un problème connu. Nous ne pouvons pas gérer toutes ces choses à Stack Overflow. En général, nous ne pouvons rien y faire. Nous ne pouvons pas non plus expliquer les décisions de mise en œuvre.