Skip to content

Aggregate over a collection navigation generates invalid SQL when a single-result subquery projects more than one member (11.0 RC1 Regression) #38834

Description

@benedict-odonovan

Bug description

An aggregate over a collection navigation generates SQL that SQL Server rejects when the aggregated expression combines a value from a FirstOrDefault() subquery with a column from the outer query:

Msg 8124: Multiple columns are specified in an aggregated expression containing an outer reference. If an expression being aggregated contains an outer reference, then that outer reference must be the only column referenced in the expression.

This is a regression: the query executes on 11.0.0-preview.6.26359.118 and fails on 11.0.0-rc.1.26410.101.

Each blog sums a value over its posts. The summed value multiplies a column read from a single-result subquery (m.Views) by a column on the outer entity (b.Rating):

var query = from b in ctx.Blogs
            let projected = from p in b.Posts
                            let m = p.Metrics.Select(x => new { x.Views, x.Likes }).FirstOrDefault()
                            select new { Views = m.Views * b.Rating, Likes = m.Likes * b.Rating }
            select new
            {
                b.Id,
                TotalViews = projected.Sum(x => x.Views),
                TotalLikes = projected.Sum(x => x.Likes)
            };

On 11.0.0-rc.1.26410.101 this produces:

SELECT [b].[Id], (
    SELECT ISNULL(SUM([m1].[Views] * [b].[Rating]), 0)
    FROM [Posts] AS [p]
    LEFT JOIN (
        SELECT [m0].[Views], [m0].[PostId]
        FROM (
            SELECT [m].[Views], [m].[PostId], ROW_NUMBER() OVER(PARTITION BY [m].[PostId] ORDER BY [m].[Id]) AS [row]
            FROM [Metrics] AS [m]
        ) AS [m0]
        WHERE [m0].[row] <= 1
    ) AS [m1] ON [p].[Id] = [m1].[PostId]
    WHERE [b].[Id] = [p].[BlogId]) AS [TotalViews], (
    SELECT ISNULL(SUM([m4].[Likes] * [b].[Rating]), 0)
    FROM [Posts] AS [p0]
    LEFT JOIN (
        SELECT [m3].[Likes], [m3].[PostId]
        FROM (
            SELECT [m2].[Likes], [m2].[PostId], ROW_NUMBER() OVER(PARTITION BY [m2].[PostId] ORDER BY [m2].[Id]) AS [row]
            FROM [Metrics] AS [m2]
        ) AS [m3]
        WHERE [m3].[row] <= 1
    ) AS [m4] ON [p0].[Id] = [m4].[PostId]
    WHERE [b].[Id] = [p0].[BlogId]) AS [TotalLikes]
FROM [Blogs] AS [b]

SUM([m1].[Views] * [b].[Rating]) sits inside a sub-query correlated on [b].[Id] = [p].[BlogId], so [b].[Rating] is an outer reference and [m1].[Views] is a second column in the same aggregated expression. SQL Server rejects that, and the query fails at execution.

On 11.0.0-preview.6.26359.118 the same LINQ produces valid SQL. The multiplication is evaluated in an OUTER APPLY, so each SUM receives exactly one column:

SELECT [b].[Id], (
    SELECT ISNULL(SUM([s].[value]), 0)
    FROM [Posts] AS [p]
    OUTER APPLY (
        SELECT (
            SELECT TOP(1) [m].[Views]
            FROM [Metrics] AS [m]
            WHERE [p].[Id] = [m].[PostId]) * [b].[Rating] AS [value]
    ) AS [s]
    WHERE [b].[Id] = [p].[BlogId]) AS [TotalViews], (
    SELECT ISNULL(SUM([s0].[value]), 0)
    FROM [Posts] AS [p0]
    OUTER APPLY (
        SELECT (
            SELECT TOP(1) [m0].[Likes]
            FROM [Metrics] AS [m0]
            WHERE [p0].[Id] = [m0].[PostId]) * [b].[Rating] AS [value]
    ) AS [s0]
    WHERE [b].[Id] = [p0].[BlogId]) AS [TotalLikes]
FROM [Blogs] AS [b]

The number of members read from the single-result subquery decides which form is generated. Projecting one member instead of two keeps the OUTER APPLY form on 11.0.0-rc.1.26410.101 and the query executes:

var query = from b in ctx.Blogs
            let projected = from p in b.Posts
                            let m = p.Metrics.Select(x => new { x.Views }).FirstOrDefault()
                            select new { Views = m.Views * b.Rating }
            select new
            {
                b.Id,
                TotalViews = projected.Sum(x => x.Views)
            };
SELECT [b].[Id], (
    SELECT ISNULL(SUM([s].[value]), 0)
    FROM [Posts] AS [p]
    OUTER APPLY (
        SELECT (
            SELECT TOP(1) [m].[Views]
            FROM [Metrics] AS [m]
            WHERE [p].[Id] = [m].[PostId]) * [b].[Rating] AS [value]
    ) AS [s]
    WHERE [b].[Id] = [p].[BlogId]) AS [TotalViews]
FROM [Blogs] AS [b]

Your code

using Microsoft.EntityFrameworkCore;

public class Blog
{
    public int Id { get; set; }
    public int Rating { get; set; }
    public List<Post> Posts { get; set; } = null!;
}

public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Metric> Metrics { get; set; } = null!;
}

public class Metric
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public Post Post { get; set; } = null!;
    public int Views { get; set; }
    public int Likes { get; set; }
}

public class AppContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post> Posts => Set<Post>();
    public DbSet<Metric> Metrics => Set<Metric>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(
            @"Server=(localdb)\MSSQLLocalDB;Database=Repro;Trusted_Connection=True;TrustServerCertificate=True");
}

public static class Program
{
    public static void Main()
    {
        using var ctx = new AppContext();

        var query = from b in ctx.Blogs
                    let projected = from p in b.Posts
                                    // Projecting only x.Views here keeps the working translation.
                                    let m = p.Metrics.Select(x => new { x.Views, x.Likes }).FirstOrDefault()
                                    select new { Views = m.Views * b.Rating, Likes = m.Likes * b.Rating }
                    select new
                    {
                        b.Id,
                        TotalViews = projected.Sum(x => x.Views),
                        TotalLikes = projected.Sum(x => x.Likes)
                    };

        Console.WriteLine(query.ToQueryString());

        ctx.Database.EnsureDeleted();
        ctx.Database.EnsureCreated();

        // Throws on 11.0.0-rc.1.26410.101, succeeds on 11.0.0-preview.6.26359.118.
        Console.WriteLine($"rows: {query.ToList().Count}");
    }
}

Stack traces

Microsoft.Data.SqlClient.SqlException (0x80131904): Multiple columns are specified in an aggregated expression containing an outer reference. If an expression being aggregated contains an outer reference, then that outer reference must be the only column referenced in the expression.
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.InitializeReader(Enumerator enumerator)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.<>c.<MoveNext>b__21_0(DbContext _, Enumerator enumerator)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Program.Main()
Error Number:8124,State:1,Class:16

Verbose output


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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions