sql - 在 Linq to Entities 中使用 COUNT 生成高效的 LEFT JOIN

标签 sql linq linq-to-entities

我有这个架构:

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

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

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

下面是与我希望 Linq 生成的 SQL 类似的内容:

SELECT
    *
FROM
(
    -- This part represents the IQueryable:

    SELECT
        ListId,
        Name,
        ...
    FROM
        Lists
    ORDER BY
        Lists.DateCreated
    OFFSET
        0 ROWS FETCH NEXT 25 ROWS ONLY -- Linq's .Skip(0).Take(25)
) AS ListsResult
LEFT JOIN
(
    -- This is the aggregate data query I want Linq to generate:
    SELECT
        ListId,
        COUNT(1) AS [Count],
        COUNT( CASE WHEN Foo = 'bar' THEN 1 ELSE NULL END ) AS CountFooBar,
        COUNT( CASE WHEN Baz > 5     THEN 1 ELSE NULL END ) AS CountBaz5
    FROM
        ListItems
    WHERE
        Qux IS NOT NULL
    GROUP BY
        ListId
) AS ItemsStats ON ListResults.ListId = ItemsStats.ListId

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

IQueryable lists = GetLists( 0, 25 );

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

return lists
    .Join(
        inner: stats,
        outerKeySelector: (List l) => l.ListId,
        innerKeySelector: grp => grp.Key,
        resultSelector: (list, itemStats) => new { list, itemsStats }
    )

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

SELECT 
    [Project13].[C2] AS [C1], 
    [Project13].[ListId] AS [ListId], 
    [Project13].[C1] AS [C2], 
    [Project13].[C3] AS [C3], 
    [Project13].[C4] AS [C4], 
    [Project13].[C5] AS [C5], 
    [Project13].[C6] AS [C6], 
    [Project13].[C7] AS [C7]
    FROM ( SELECT 
        [Project11].[C1] AS [C1], 
        [Project11].[ListId] AS [ListId], 
        [Project11].[C2] AS [C2], 
        [Project11].[C3] AS [C3], 
        [Project11].[C4] AS [C4], 
        [Project11].[C5] AS [C5], 
        [Project11].[C6] AS [C6], 
        (SELECT 
            COUNT(1) AS [A1]
            FROM   (SELECT [Project12].[ListId] AS [ListId]
                FROM ( SELECT 
                    [Extent11].[ListId] AS [ListId], 
                    [Extent11].[Created] AS [Created]
                    FROM [dbo].[Lists] AS [Extent11]
                    WHERE ([Extent11].[TenantId] = 8) AND ([Extent11].[BlarghId] = 8)
                )  AS [Project12]
                ORDER BY [Project12].[Created] DESC
                OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [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] IS NOT NULL)) AS [C7]
        FROM ( SELECT 
            [Project9].[C1] AS [C1], 
            [Project9].[TenantId] AS [TenantId], 
            [Project9].[BlarghId] AS [BlarghId], 
            [Project9].[ListId] AS [ListId], 
            [Project9].[C2] AS [C2], 
            [Project9].[C3] AS [C3], 
            [Project9].[C4] AS [C4], 
            [Project9].[C5] AS [C5], 
            (SELECT 
                COUNT(1) AS [A1]
                FROM   (SELECT [Project10].[TenantId] AS [TenantId], [Project10].[BlarghId] AS [BlarghId], [Project10].[ListId] AS [ListId]
                    FROM ( SELECT 
                        [Extent9].[TenantId] AS [TenantId], 
                        [Extent9].[BlarghId] AS [BlarghId], 
                        [Extent9].[ListId] AS [ListId], 
                        [Extent9].[Created] AS [Created]
                        FROM [dbo].[Lists] AS [Extent9]
                        WHERE ([Extent9].[TenantId] = 8) AND ([Extent9].[BlarghId] = 8)
                    )  AS [Project10]
                    ORDER BY [Project10].[Created] DESC
                    OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [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]
            FROM ( SELECT 
                [Project7].[C1] AS [C1], 
                [Project7].[TenantId] AS [TenantId], 
                [Project7].[BlarghId] AS [BlarghId], 
                [Project7].[ListId] AS [ListId], 
                [Project7].[C2] AS [C2], 
                [Project7].[C3] AS [C3], 
                [Project7].[C4] AS [C4], 
                (SELECT 
                    COUNT(1) AS [A1]
                    FROM   (SELECT [Project8].[TenantId] AS [TenantId], [Project8].[BlarghId] AS [BlarghId], [Project8].[ListId] AS [ListId]
                        FROM ( SELECT 
                            [Extent7].[TenantId] AS [TenantId], 
                            [Extent7].[BlarghId] AS [BlarghId], 
                            [Extent7].[ListId] AS [ListId], 
                            [Extent7].[Created] AS [Created]
                            FROM [dbo].[Lists] AS [Extent7]
                            WHERE ([Extent7].[TenantId] = 8) AND ([Extent7].[BlarghId] = 8)
                        )  AS [Project8]
                        ORDER BY [Project8].[Created] DESC
                        OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [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]
                FROM ( SELECT 
                    [Project5].[C1] AS [C1], 
                    [Project5].[TenantId] AS [TenantId], 
                    [Project5].[BlarghId] AS [BlarghId], 
                    [Project5].[ListId] AS [ListId], 
                    [Project5].[C2] AS [C2], 
                    [Project5].[C3] AS [C3], 
                    (SELECT 
                        COUNT(1) AS [A1]
                        FROM   (SELECT [Project6].[TenantId] AS [TenantId], [Project6].[BlarghId] AS [BlarghId], [Project6].[ListId] AS [ListId]
                            FROM ( SELECT 
                                [Extent5].[TenantId] AS [TenantId], 
                                [Extent5].[BlarghId] AS [BlarghId], 
                                [Extent5].[ListId] AS [ListId], 
                                [Extent5].[Created] AS [Created]
                                FROM [dbo].[Lists] AS [Extent5]
                                WHERE ([Extent5].[TenantId] = 8) AND ([Extent5].[BlarghId] = 8)
                            )  AS [Project6]
                            ORDER BY [Project6].[Created] DESC
                            OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [Limit3]
                        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]
                    FROM ( SELECT 
                        [Project3].[C1] AS [C1], 
                        [Project3].[TenantId] AS [TenantId], 
                        [Project3].[BlarghId] AS [BlarghId], 
                        [Project3].[ListId] AS [ListId], 
                        [Project3].[C2] AS [C2], 
                        (SELECT 
                            COUNT(1) AS [A1]
                            FROM   (SELECT [Project4].[TenantId] AS [TenantId], [Project4].[BlarghId] AS [BlarghId], [Project4].[ListId] AS [ListId]
                                FROM ( SELECT 
                                    [Extent3].[TenantId] AS [TenantId], 
                                    [Extent3].[BlarghId] AS [BlarghId], 
                                    [Extent3].[ListId] AS [ListId], 
                                    [Extent3].[Created] AS [Created]
                                    FROM [dbo].[Lists] AS [Extent3]
                                    WHERE ([Extent3].[TenantId] = 8) AND ([Extent3].[BlarghId] = 8)
                                )  AS [Project4]
                                ORDER BY [Project4].[Created] DESC
                                OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [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]
                        FROM ( SELECT 
                            [GroupBy1].[A1] AS [C1], 
                            [GroupBy1].[K1] AS [TenantId], 
                            [GroupBy1].[K2] AS [BlarghId], 
                            [GroupBy1].[K3] AS [ListId], 
                            [GroupBy1].[K4] AS [C2]
                            FROM ( SELECT 
                                [Project2].[K1] AS [K1], 
                                [Project2].[K2] AS [K2], 
                                [Project2].[K3] AS [K3], 
                                [Project2].[K4] AS [K4], 
                                COUNT([Project2].[A1]) AS [A1]
                                FROM ( SELECT 
                                    [Project2].[TenantId] AS [K1], 
                                    [Project2].[BlarghId] AS [K2], 
                                    [Project2].[ListId] AS [K3], 
                                    1 AS [K4], 
                                    1 AS [A1]
                                    FROM ( SELECT 
                                        [Extent2].[ListId] AS [ListId]
                                        FROM   (SELECT [Project1].[ListId] AS [ListId]
                                            FROM ( SELECT 
                                                [Extent1].[ListId] AS [ListId], 
                                                [Extent1].[Created] AS [Created]
                                                FROM [dbo].[Lists] AS [Extent1]
                                                WHERE ([Extent1].[TenantId] = 8) AND ([Extent1].[BlarghId] = 8)
                                            )  AS [Project1]
                                            ORDER BY [Project1].[Created] DESC
                                            OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY  ) AS [Limit1]
                                        INNER JOIN [dbo].[ListItems] AS [Extent2] ON (([Limit1].[ListId] = [Extent2].[ListId])
                                        WHERE ([Extent2].[Baz] > 0) OR ((LEN([Extent2].[Notes])) > 0) OR ((LEN([Extent2].[Value])) > 0)
                                    )  AS [Project2]
                                )  AS [Project2]
                                GROUP BY [K1], [K2], [K3], [K4]
                            )  AS [GroupBy1]
                        )  AS [Project3]
                    )  AS [Project5]
                )  AS [Project7]
            )  AS [Project9]
        )  AS [Project11]
    )  AS [Project13]

它不构成 COUNT()语句完全放在一起,并且它移动了 COUNT分隔 WHERE 的谓词条款。另请注意重复的分页子查询(其中使用 OFFSET 0 ROW FETCH NEXT 25),而我的手写查询仅执行一次。

最佳答案

这是我的半解决方法:

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

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

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

这是我使用的:

CREATE VIEW ListItemStatistics AS

SELECT
    ListId,

    COUNT(*) AS [CountAll],
    COUNT( CASE WHEN ... ) AS Count...

FROM
    ListItems
WHERE
    Foo = 'bar'
GROUP BY
     ListId

然后从 Linq 内部:

IQueryable lists = GetListsQuery( 0, 25 );

var listsWithItemsStats = lists.
    .Join(
        inner: this.dbContext.ListItemStatistics,
        outerKeySelector: list => list.ListId,
        innerKeySelector: row => row.ListId,
        resultSelector: (list,row) => new { list, row }
    );

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

关于sql - 在 Linq to Entities 中使用 COUNT 生成高效的 LEFT JOIN,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38410635/

相关文章:

C# Generic ToString Join if Array or IEnumerable

c# - Linq ForEach() 不填充字段

linq-to-entities - Entity Framework - 通过集合导航和包含属性

c# - 从 LINQ 实体中的日期时间获取可排序字符串的正确方法是什么?

mysql - 需要有关主键选择的建议

mysql - 添加触发器的正确​​语法

c# - 这是从 DataContext 更新实体的最佳方式吗?

sql - 按名字和姓氏搜索的 Postgresql 查询,我应该创建哪些索引?

sql - 涉及类似异或条件的 SQL 查询问题

c# - 使用 LINQ to SQL 将具有外键的 Gridview 列排序到不同的实体