【问题标题】:Generating efficient LEFT JOIN with COUNT in Linq to Entities在 Linq to Entities 中使用 COUNT 生成高效的 LEFT JOIN
【发布时间】:2016-11-19 12:30:46
【问题描述】:

我有这个架构:

Lists ( ListId, Name, DateCreated, ... )
ListItems( ListId, Text, Foo, Baz, Qux, ... )

我有一个 IQueryable<List> 代表另一个 Linq 查询,它返回一些 List 实体。

我想用ListItems 的一些聚合数据来JOIN 它,但事实证明这很困难,因为 Linq 生成的 SQL 效率低下 - 但我也想让查询可组合。

这是类似于我希望 Linq 生成的 SQL:

选择
    *
从
(
    -- 这部分代表 IQueryable:

    选择
        列表 ID,
        姓名,
        ...
    从
        列表
    订购方式
        Lists.DateCreated
    抵消
        0 ROWS 仅获取下一个 25 行——Linq 的 .Skip(0).Take(25)
) AS ListsResult
左连接
(
    -- 这是我希望 Linq 生成的聚合数据查询:
    选择
        列表 ID,
        计数(1)作为[计数],
        计数(当 Foo = 'bar' THEN 1 ELSE NULL END 时的情况)作为 CountFooBar,
        计数(当 Baz > 5 然后 1 ELSE NULL END 时的情况)作为 CountBaz5
    从
        列表项
    在哪里
        Qux 不为空
    通过...分组
        列表 ID
) AS ItemsStats ON ListResults.ListId = ItemsStats.ListId

这是我拥有的 Linq - 我更喜欢扩展方法语法:

IQueryable 列表 = GetLists(0, 25);

var stats = this.dbContext.ListItems
    .Where( (ListItem li) => li.Qux != null )
    .GroupBy( (ListItem li) => li.ListId )
    .Select(grp => 新
    {
        grp.Key,
        计数 = grp.Count(),
        CountFooBar = grp.Count( (ListItem li) => li.Foo == "bar" )
        CountBaz5 = grp.Count( (ListItem li) => li.Baz > 5 )
    })

返回列表
    。加入(
        内部:统计数据,
        outerKeySelector: (List l) => l.ListId,
        innerKeySelector: grp => grp.Key,
        resultSelector: (list, itemStats) => new { list, itemsStats }
    )

但是,这会生成如下所示的 SQL(此查询显示了我的真实表和列名,这比我之前发布的架构要复杂一些:)

选择 [项目 13].[C2] 作为 [C1], [Project13].[ListId] AS [ListId], [项目 13].[C1] 作为 [C2], [项目 13].[C3] 作为 [C3], [项目 13].[C4] 作为 [C4], [项目 13].[C5] 作为 [C5], [项目 13].[C6] 作为 [C6], [项目 13].[C7] 作为 [C7] 从(选择 [项目 11].[C1] 作为 [C1], [Project11].[ListId] AS [ListId], [项目 11].[C2] 作为 [C2], [项目 11].[C3] 作为 [C3], [项目 11].[C4] 作为 [C4], [项目 11].[C5] 作为 [C5], [项目11].[C6] AS [C6], (选择 计数(1) 作为 [A1] FROM (SELECT [Project12].[ListId] AS [ListId] 从(选择 [Extent11].[ListId] AS [ListId], [Extent11].[已创建] AS [已创建] FROM [dbo].[Lists] AS [Extent11] WHERE ([Extent11].[TenantId] = 8) AND ([Extent11].[BlarghId] = 8) ) 作为 [项目 12] ORDER BY [Project12].[Created] DESC 偏移量 0 行仅获取下一个 25 行)作为 [Limit6] INNER JOIN [dbo].[ListItems] AS [Extent12] ON ([Limit6].[TenantId] = [Extent12].[TenantId]) AND ([Limit6].[BlarghId] = [Extent12].[BlarghId]) AND ([Limit6].[ListId] = [Extent12].[ListId]) WHERE (([Extent12].[Baz] > 0) OR ((LEN([Extent12].[Notes])) > 0) OR ((LEN([Extent12].[Value])) > 0)) AND ( [Project11].[TenantId] = [Extent12].[TenantId]) AND ([Project11].[BlarghId] = [Extent12].[BlarghId]) AND ([Project11].[ListId] = [Extent12].[ListId ]) AND ([Extent12].[RecommendationRevision] 不为空)) 作为 [C7] 从(选择 [项目 9].[C1] 作为 [C1], [Project9].[TenantId] 作为 [TenantId], [项目 9].[BlarghId] 作为 [BlarghId], [Project9].[ListId] AS [ListId], [项目 9].[C2] 作为 [C2], [项目 9].[C3] 作为 [C3], [项目 9].[C4] 作为 [C4], [项目 9].[C5] 作为 [C5], (选择 计数(1) 作为 [A1] FROM (SELECT [Project10].[TenantId] AS [TenantId], [Project10].[BlarghId] AS [BlarghId], [Project10].[ListId] AS [ListId] 从(选择 [Extent9].[TenantId] AS [TenantId], [Extent9].[BlarghId] 作为 [BlarghId], [Extent9].[ListId] AS [ListId], [Extent9].[已创建] AS [已创建] FROM [dbo].[Lists] AS [Extent9] WHERE ([Extent9].[TenantId] = 8) AND ([Extent9].[BlarghId] = 8) ) 作为 [Project10] ORDER BY [Project10].[Created] DESC 偏移量 0 行仅获取下一个 25 行)作为 [Limit5] INNER JOIN [dbo].[ListItems] AS [Extent10] ON ([Limit5].[TenantId] = [Extent10].[TenantId]) AND ([Limit5].[BlarghId] = [Extent10].[BlarghId]) AND ([Limit5].[ListId] = [Extent10].[ListId]) WHERE (([Extent10].[Baz] > 0) OR ((LEN([Extent10].[Notes])) > 0) OR ((LEN([Extent10].[Value])) > 0)) AND ( [Project9].[TenantId] = [Extent10].[TenantId]) AND ([Project9].[BlarghId] = [Extent10].[BlarghId]) AND ([Project9].[ListId] = [Extent10].[ListId ]) AND (3 = [Extent10].[Baz])) AS [C6] 从(选择 [项目 7].[C1] 作为 [C1], [Project7].[TenantId] 作为 [TenantId], [项目 7].[BlarghId] 作为 [BlarghId], [Project7].[ListId] AS [ListId], [项目7].[C2] AS [C2], [项目 7].[C3] 作为 [C3], [项目 7].[C4] 作为 [C4], (选择 计数(1) 作为 [A1] FROM (SELECT [Project8].[TenantId] AS [TenantId], [Project8].[BlarghId] AS [BlarghId], [Project8].[ListId] AS [ListId] 从(选择 [Extent7].[TenantId] 作为 [TenantId], [Extent7].[BlarghId] 作为 [BlarghId], [Extent7].[ListId] AS [ListId], [Extent7].[已创建] AS [已创建] FROM [dbo].[Lists] AS [Extent7] WHERE ([Extent7].[TenantId] = 8) AND ([Extent7].[BlarghId] = 8) ) 作为 [项目8] ORDER BY [Project8].[Created] DESC 偏移量 0 行仅获取下一个 25 行)作为 [Limit4] INNER JOIN [dbo].[ListItems] AS [Extent8] ON ([Limit4].[TenantId] = [Extent8].[TenantId]) AND ([Limit4].[BlarghId] = [Extent8].[BlarghId]) AND ([Limit4].[ListId] = [Extent8].[ListId]) WHERE (([Extent8].[Baz] > 0) OR ((LEN([Extent8].[Notes])) > 0) OR ((LEN([Extent8].[Value])) > 0)) AND ( [Project7].[TenantId] = [Extent8].[TenantId]) AND ([Project7].[BlarghId] = [Extent8].[BlarghId]) AND ([Project7].[ListId] = [Extent8].[ListId ]) AND (2 = [Extent8].[Baz])) AS [C5] 从(选择 [项目 5].[C1] 作为 [C1], [Project5].[TenantId] 作为 [TenantId], [项目 5].[BlarghId] 作为 [BlarghId], [Project5].[ListId] AS [ListId], [项目5].[C2] AS [C2], [项目 5].[C3] 作为 [C3], (选择 计数(1) 作为 [A1] FROM (SELECT [Project6].[TenantId] AS [TenantId], [Project6].[BlarghId] AS [BlarghId], [Project6].[ListId] AS [ListId] 从(选择 [Extent5].[TenantId] AS [TenantId], [Extent5].[BlarghId] 作为 [BlarghId], [Extent5].[ListId] AS [ListId], [Extent5].[已创建] AS [已创建] FROM [dbo].[Lists] AS [Extent5] WHERE ([Extent5].[TenantId] = 8) AND ([Extent5].[BlarghId] = 8) ) 作为 [项目6] ORDER BY [Project6].[Created] DESC 偏移量 0 行仅获取下一个 25 行)作为 [限制 3] INNER JOIN [dbo].[ListItems] AS [Extent6] ON ([Limit3].[TenantId] = [Extent6].[TenantId]) AND ([Limit3].[BlarghId] = [Extent6].[BlarghId]) AND ([Limit3].[ListId] = [Extent6].[ListId]) WHERE (([Extent6].[Baz] > 0) OR ((LEN([Extent6].[Notes])) > 0) OR ((LEN([Extent6].[Value])) > 0)) AND ( [Project5].[TenantId] = [Extent6].[TenantId]) AND ([Project5].[BlarghId] = [Extent6].[BlarghId]) AND ([Project5].[ListId] = [Extent6].[ListId ]) AND (1 = [Extent6].[Baz])) AS [C4] 从(选择 [项目 3].[C1] 作为 [C1], [Project3].[TenantId] 作为 [TenantId], [项目 3].[BlarghId] 作为 [BlarghId], [Project3].[ListId] AS [ListId], [项目3].[C2] AS [C2], (选择 计数(1) 作为 [A1] FROM (SELECT [Project4].[TenantId] AS [TenantId], [Project4].[BlarghId] AS [BlarghId], [Project4].[ListId] AS [ListId] 从(选择 [Extent3].[TenantId] AS [TenantId], [Extent3].[BlarghId] 作为 [BlarghId], [Extent3].[ListId] AS [ListId], [Extent3].[已创建] AS [已创建] FROM [dbo].[Lists] AS [Extent3] WHERE ([Extent3].[TenantId] = 8) AND ([Extent3].[BlarghId] = 8) ) 作为 [项目4] ORDER BY [Project4].[Created] DESC 偏移量 0 行仅获取下一个 25 行)作为 [Limit2] INNER JOIN [dbo].[ListItems] AS [Extent4] ON ([Limit2].[TenantId] = [Extent4].[TenantId]) AND ([Limit2].[BlarghId] = [Extent4].[BlarghId]) AND ([Limit2].[ListId] = [Extent4].[ListId]) WHERE (([Extent4].[Baz] > 0) OR ((LEN([Extent4].[Notes])) > 0) OR ((LEN([Extent4].[Value])) > 0)) AND ( [Project3].[TenantId] = [Extent4].[TenantId]) AND ([Project3].[BlarghId] = [Extent4].[BlarghId]) AND ([Project3].[ListId] = [Extent4].[ListId ]) AND ([Extent4].[Foo] = 1)) AS [C3] 从(选择 [GroupBy1].[A1] AS [C1], [GroupBy1].[K1] AS [TenantId], [GroupBy1].[K2] AS [BlarghId], [GroupBy1].[K3] AS [ListId], [GroupBy1].[K4] AS [C2] 从(选择 [项目2].[K1] AS [K1], [项目2].[K2] AS [K2], [项目2].[K3] AS [K3], [项目2].[K4] AS [K4], 计数([项目 2].[A1])作为 [A1] 从(选择 [Project2].[TenantId] AS [K1], [项目2].[BlarghId] AS [K2], [Project2].[ListId] AS [K3], 1 AS [K4], 1 作为 [A1] 从(选择 [Extent2].[ListId] AS [ListId] FROM (SELECT [Project1].[ListId] AS [ListId] 从(选择 [Extent1].[ListId] AS [ListId], [Extent1].[已创建] AS [已创建] FROM [dbo].[Lists] AS [Extent1] WHERE ([Extent1].[TenantId] = 8) AND ([Extent1].[BlarghId] = 8) ) 作为 [项目1] ORDER BY [Project1].[Created] DESC 偏移量 0 行仅获取下一个 25 行)作为 [Limit1] 内部联接 [dbo].[ListItems] AS [Extent2] ON (([Limit1].[ListId] = [Extent2].[ListId]) WHERE ([Extent2].[Baz] > 0) OR ((LEN([Extent2].[Notes])) > 0) OR ((LEN([Extent2].[Value])) > 0) ) 作为 [项目2] ) 作为 [项目2] 按 [K1]、[K2]、[K3]、[K4] 分组 ) AS [GroupBy1] ) 作为 [项目3] ) 作为 [项目5] ) 作为 [项目7] ) 作为 [项目 9] ) 作为 [Project11] ) 作为 [项目 13]

它根本不会将COUNT() 语句组合在一起,而是将COUNT 谓词移动到单独的WHERE 子句中。还要注意重复的分页子查询(其中使用了OFFSET 0 ROW FETCH NEXT 25),而我的手写查询只执行一次。

【问题讨论】:

    标签: sql linq linq-to-entities


    【解决方案1】:

    根据我的经验,当您在您的案例中使用像 Count(predicate 一样应用过滤的聚合函数时,EF 会生成此类查询。如果将 Count(condition) 构造替换为条件和 (Sum(condition ? 1 : 0)),您将获得更好的 SQL 查询,如下所示:

    var stats = db.ListItems
        .Where(li => li.Qux != null)
        .GroupBy(li => li.ListId)
        .Select(grp => new
        {
            grp.Key,
            Count = grp.Count(),
            CountFooBar = grp.Sum(li => li.Foo == "bar" ? 1 : 0),
            CountBaz5 = grp.Sum(li => li.Baz > 5 ? 1 : 0)
        });
    

    其他部分保持不变。在 mod 之前,我得到了与发布的类似的 SQL,这是我在这个小 mod 之后得到的:

    SELECT 
        [Limit1].[ListId] AS [ListId], 
        [Limit1].[Name] AS [Name], 
        [Limit1].[DateCreated] AS [DateCreated], 
        [GroupBy1].[K1] AS [ListId1], 
        [GroupBy1].[A1] AS [C1], 
        [GroupBy1].[A2] AS [C2], 
        [GroupBy1].[A3] AS [C3]
        FROM   (SELECT [Extent1].[ListId] AS [ListId], [Extent1].[Name] AS [Name], [Extent1].[DateCreated] AS [DateCreated]
            FROM [dbo].[List] AS [Extent1]
            ORDER BY [Extent1].[DateCreated] ASC
            OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [Limit1]
        INNER JOIN  (SELECT 
            [Filter1].[K1] AS [K1], 
            COUNT([Filter1].[A1]) AS [A1], 
            SUM([Filter1].[A2]) AS [A2], 
            SUM([Filter1].[A3]) AS [A3]
            FROM ( SELECT 
                [Extent2].[ListId] AS [K1], 
                1 AS [A1], 
                CASE WHEN (N'bar' = [Extent2].[Foo]) THEN 1 ELSE 0 END AS [A2], 
                CASE WHEN ([Extent2].[Baz] > 5) THEN 1 ELSE 0 END AS [A3]
                FROM [dbo].[ListItem] AS [Extent2]
                WHERE [Extent2].[Qux] IS NOT NULL
            )  AS [Filter1]
            GROUP BY [K1] ) AS [GroupBy1] ON [Limit1].[ListId] = [GroupBy1].[K1]
    

    更新:以上只是您手动 SQL 查询的 LINQ 等效项(当您调整连接代码以生成 LEFT OUTER JOIN 时)。但是,考虑到主数据分页,OUTER APPLY SQL 查询可能会执行得更好。您可以让 LINQ to Entities 生成这样的查询类型:

    var listsWithItemsStats = lists
        .SelectMany(l => db.ListItems
            .Where(li => li.ListId == l.ListId && li.Qux != null)
            .GroupBy(li => li.ListId)
            .Select(grp => new
            {
                grp.Key,
                Count = grp.Count(),
                CountFooBar = grp.Sum(li => li.Foo == "bar" ? 1 : 0),
                CountBaz5 = grp.Sum(li => li.Baz > 5 ? 1 : 0)
            })
            .DefaultIfEmpty(),
            (list, itemsStats) => new { list, itemsStats });
    

    【讨论】:

    • 有趣的是它如何对SUM 计算进行内部查询。我想看看执行计划,因为看起来这也可能效率低下(因为内部查询将为每个 List 组生成数千行)。
    • 是的。但这同样适用于您的手动 SQL 查询。以上只是该查询的 LINQ 等效项(当您修改连接代码以生成 LEFT OUTER JOIN 时)。性能方面,考虑到主分页,OUTER APPLY SQL 查询可能会更好。
    【解决方案2】:

    这是我的一个半解决方法:

    我意识到最好的短期解决方案是将 SQL 放在数据库中(作为 UDF FUNCTIONVIEW),这意味着一些数据代码因此必须在数据库中(而不是将数据库用作“哑存储”)。

    我首先创建了一个表值 UDF,它接受一个表值参数,推理允许组合,代价是需要生成输入参数表(来自分页查询的 ListId 值数组)。但是在这样做的过程中,我意识到 Linq-to-Entities(版本 6)还不支持函数导入中的表值参数。

    然后我推断一个更好的方法是将COUNT 操作移动到VIEW(它代表我手写查询中LEFT JOIN 的一半),然后我可以让Linq针对现有的IQueryable 加入它,从而保持可组合性并生成高效的运行时查询(实际上,当我运行它时,根据 SQL Server Profiler 执行查询需要 34 毫秒,而旧的 Linq 生成的低效查询需要 830 毫秒)。

    这是我使用的:

    CREATE VIEW ListItemStatistics AS 选择 列表 ID, COUNT(*) AS [CountAll], 计数(CASE WHEN ...)作为计数... 从 列表项 在哪里 Foo = '酒吧' 通过...分组 列表 ID

    然后从 Linq 内部:

    IQueryable 列表 = GetListsQuery( 0, 25 ); var listsWithItemsStats = 列表。 。加入( 内部:this.dbContext.ListItemStatistics, outerKeySelector: list => list.ListId, innerKeySelector: row => row.ListId, resultSelector: (list,row) => new { list, row } );

    但是,因为这确实使用了数据库端逻辑(在 VIEW 中),所以并不理想。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-30
      • 2011-04-26
      • 1970-01-01
      • 2011-05-12
      • 1970-01-01
      相关资源
      最近更新 更多