镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Lift GroupBy aggregates over navigations next to operator chains - #39648

Open
seragAhmed wants to merge 2 commits into
dotnet:mainfrom
seragAhmed:fix/39628-lift-aggregates-with-non-foldable-siblings
Open

seragAhmed wants to merge 2 commits into
dotnet:mainfrom
seragAhmed:fix/39628-lift-aggregates-with-non-foldable-siblings

Conversation

@seragAhmed

Copy link
Copy Markdown

Fixes #39628

The lift of GroupBy aggregates over reference navigations (#38668) is all or nothing: GroupingAggregateScanner only recognizes aggregates applied directly to the grouping, and any other use of the grouping makes TryLiftAggregatesOverNavigations give up for the whole projection. So one g.Select(x => x.Value).Distinct().Count() next to g.Max(x => x.Author.Rating) turned every navigation aggregate back into a correlated subquery.

The scanner now also accepts a short operator chain before the aggregate: Where over the element, at most one Select, and Distinct after the Select. The lambdas over the element are expanded on the parent like aggregate selectors, and the chain is rebuilt over the lifted grouping. After a Select, only aggregates without a selector or predicate are accepted (including the non-generic numeric overloads such as Sum(IEnumerable<int?>)), since a lambda over the projected values would not have its navigations expanded. Other uses of the grouping keep the existing translation.

ctx.Orders.GroupBy(o => o.EmployeeID).Select(g => new
{
    g.Key,
    Region = g.Max(o => o.Customer!.Region),
    Customers = g.Select(o => o.CustomerID).Distinct().Count()
});
-- before: MAX through a correlated subquery; after:
SELECT [o].[EmployeeID] AS [Key], MAX([c].[Region]) AS [Region], COUNT(DISTINCT ([o].[CustomerID])) AS [Customers]
FROM [Orders] AS [o]
LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID]
GROUP BY [o].[EmployeeID]

Two cases are deliberately left alone:

  • Distinct over a navigation is not lifted. The correlated translation counts the null of a missing principal as a value (COUNT(*) over SELECT DISTINCT), while the lifted one would use COUNT(DISTINCT ...) and skip it. Group_by_multiple_aggregate_joining_different_tables keeps its baseline.
  • For a principal relaxed to an outer join because of its query filter (GroupBy aggregate over a required navigation with a query filter drops entire groups (Regression in 11.0) #38965), an aggregate with a chain is always guarded, since Where, Select and Distinct can observe the null-extended row.

Parameter_collection_Contains_with_projection_and_ordering uses g.Select(e => e.Order.OrderDate).Max(), which is now lifted too; its SQL Server baseline is updated (one INNER JOIN instead of two correlated subqueries).

Tests:

  • NorthwindGroupByQueryTestBase: distinct count, select-then-aggregate and where-then-count next to navigation aggregates (lifted), and a predicate after Select (still correlated), with SQL Server baselines.
  • AdHocQueryFiltersQueryRelationalTestBase: the three chain kinds over a required navigation to a filtered principal, with SQL Server and SQLite baselines.

Locally, the SQL Server and SQLite Microsoft.EntityFrameworkCore.Query* functional tests, the InMemory GroupBy tests and EFCore.Tests pass.

🤖 Generated with Claude Code

- Accept Where, Select and Distinct chains before a grouping aggregate,
  so one of them no longer forces every navigation aggregate in the
  projection into a correlated subquery.
- Keep Distinct over a navigation on the correlated translation, which
  counts the null of a missing principal.
- Guard chained aggregates over relaxed filtered principals.

Fixes dotnet#39628

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@seragAhmed

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

1 similar comment
@seragAhmed

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A single non-foldable grouped aggregate forces all sibling navigation aggregates in the same projection into correlated subqueries

1 participant