Reuse a single-result subquery across its projected members#38502
Merged
Conversation
…otnet#7776) A projection that reads two or more members of one correlated First/Single/ Last/ElementAt subquery now lifts to a single SelectMany during navigation expansion, so the members come from a single join instead of a duplicated subquery per member.
There was a problem hiding this comment.
Pull request overview
This PR introduces a targeted navigation-expansion rewrite to avoid duplicating correlated single-result subqueries when multiple members are projected from the same subquery (e.g. c.City, c.Country, c.ContactName), by lifting the subquery into a single join/SelectMany shape so all projected members come from the same joined row.
Changes:
- Add
LiftSingleResultSubqueriesto rewrite repeated member-access projections over the same correlated single-result subquery into a singleSelectMany. - Invoke the lifting pass after
ProcessSelectinNavigationExpandingExpressionVisitorforQueryable.Select. - Add a cross-provider spec test plus SQL Server SQL baseline coverage; Cosmos asserts translation failure.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.SingleResultSubqueryLifting.cs | New lifting pass to detect repeated member accesses over correlated single-result subqueries and rewrite to a single join via SelectMany. |
| src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs | Hooks the lifting pass into the Queryable.Select handling path. |
| test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs | Adds the new scenario as a provider-agnostic spec test across single-result operators. |
| test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs | Adds SQL Server baseline assertions verifying the lifted single-join translation shape. |
| test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs | Marks the new test as translation-failing for Cosmos. |
AndriySvyryd
approved these changes
Jun 30, 2026
AndriySvyryd
left a comment
Member
There was a problem hiding this comment.
Thanks for your contribution!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I've put together a small interim fix for #7776 and wanted to share it for your consideration. I'd completely understand if you'd rather wait for the pending-selector rework in #20291 (which you've noted #7776 is probably a duplicate of). But since that's a larger effort, a self-contained stopgap might be useful in the meantime.
I've kept it intentionally scoped to a single ~100-line file, plus a one-line call in
NavigationExpandingExpressionVisitor. Easy to replace in the future when the root fix lands.It fires only when a projection reads two or more members of one correlated single-result subquery, and rewrites that into a
SelectManyso the members come from one join. Everything else is left untouched. It doesn't address the general reused-subquery case (#20291 would), only this shape.Tested across the single-result operators (
First/Single/Last/ElementAtand theirOrDefault/predicate forms) on SQL Server, SQLite and the in-memory provider; the full SQL Server suite passes with no baseline changes.