GroupBy with an element selector prevents aggregates over reference navigations from being lifted into a join
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
In EF Core 11, `GroupBy(key).Select(g => g.Max(o => o.Nav.Property))` now translates to a single `SELECT` with a join (#27933, fixed by #38668). Writing the same query with a `GroupBy` element selector still produces a correlated sub-query per aggregate, as EF Core 10 did.
The two LINQ queries return the same results and look equivalent in LINQ, but the SQL is very different.
```csharp
db.Orders
.GroupBy(o => o.CustomerId)
.Select(g => new
{
g.Key,
Region = g.Max(o => o.Customer.Region),
Discount = g.Max(o => o.Customer.Discount),
Amount = g.Sum(o => o.Amount),
});
```
Produces:
```sql
SELECT [o].[CustomerId] AS [Key], MAX([c].[Region]) AS [Region], MAX([c].[Discount]) AS [Discount], COALESCE(SUM([o].[Amount]), 0.0) AS [Amount]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
GROUP BY [o].[CustomerId]
```
while
```csharp
db.Orders
.GroupBy(o => o.CustomerId, o => new { o.Customer.Region, o.Customer.Discount, o.Amount })
.Select(g => new
{
g.Key,
Region = g.Max(x => x.Region),
Discount = g.Max(x => x.Discount),
Amount = g.Sum(x => x.Amount),
});
```
produces:
```sql
SELECT [o].[CustomerId] AS [Key], (
SELECT MAX([c].[Region])
FROM [Orders] AS [o0]
INNER JOIN [Customers] AS [c] ON [o0].[CustomerId] = [c].[Id]
WHERE [o].[CustomerId] = [o0].[CustomerId]) AS [Region], (
SELECT MAX([c0].[Discount])
FROM [Orders] AS [o1]
INNER JOIN [Customers] AS [c0] ON [o1].[CustomerId] = [c0].[Id]
WHERE [o].[CustomerId] = [o1].[CustomerId]) AS [Discount], COALESCE(SUM([o].[Amount]), 0.0) AS [Amount]
FROM [Orders] AS [o]
GROUP BY [o].[CustomerId]
```
### Your code
```csharp
using Microsoft.EntityFrameworkCore;
using var db = new AppContext();
Print("1. aggregates over a reference navigation",
db.Orders
.GroupBy(o => o.CustomerId)
.Select(g => new
{
g.Key,
Region = g.Max(o => o.Customer.Region),
Discount = g.Max(o => o.Customer.Discount),
Amount = g.Sum(o => o.Amount),
}));
Print("2. the same aggregates, reached through a GroupBy element selector",
db.Orders
.GroupBy(o => o.CustomerId, o => new { o.Customer.Region, o.Customer.Discount, o.Amount })
.Select(g => new
{
g.Key,
Region = g.Max(x => x.Region),
Discount = g.Max(x => x.Discount),
Amount = g.Sum(x => x.Amount),
}));
static void Print(string title, IQueryable query)
{
var sql = query.ToQueryString();
var selects = System.Text.RegularExpressions.Regex.Matches(sql, @"\bSELECT\b").Count;
Console.WriteLine($"===== {title} [{selects} SELECT(s)] =====");
Console.WriteLine(sql);
Console.WriteLine();
}
public class Customer
{
public int Id { get; set; }
public string Region { get; set; } = null!;
public decimal Discount { get; set; }
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
public decimal Amount { get; set; }
public decimal? Freight { get; set; }
}
public class AppContext : DbContext
{
public DbSet Customers => Set();
public DbSet Orders => Set();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("Server=localhost;Database=Repro;Trusted_Connection=True;TrustServerCertificate=True");
}
```
### Stack traces
```text
```
### Verbose output
```text
```
### EF Core version
11.0.0-rc.1.26410.101
### Database provider
Microsoft.EntityFrameworkCore.SqlServer
### Target framework
.NET 11
### Operating system
Windows 11
### IDE
_No response_
Contributor guide
Research direction
Start with the two GroupBy LINQ queries and their ToQueryString output, comparing the join-based translation with the correlated subqueries produced for the element selector. Trace the GroupBy query translation entry points and add coverage for the SQL shape; done means reference-navigation aggregates use a single joined SELECT like the equivalent query.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, sql
- Domain
- database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100