LINQ 和实体框架 - 避免子查询

本文关键字:查询 实体 框架 LINQ | 更新日期: 2023-09-27 18:14:13

我很难在我的应用程序中调整我Entity Framework生成的查询之一。这是非常基本的查询,但由于某种原因EF使用多个内部子查询,这些子查询似乎在DB中表现得很糟糕,而不是使用连接。

这是我的 LINQ 代码:

Projects.Select(proj => new ProjectViewModel()
                {
                    Name = proj.Name,
                    Id = proj.Id,
                    Total = proj.Subvalue.Where(subv =>
                        subv.Created >= startDate
                        && subv.Created <= endDate
                        &&
                        (subv.StatusId == 1 ||
                         subv.StatusId == 2))
                        .Select(c => c.SubValueSum)
                        .DefaultIfEmpty()
                        .Sum()
                })
                .OrderByDescending(c => c.Total)
                .Take(10);

EF 生成具有多个子查询的非常复杂的查询,其查询性能非常糟糕,如下所示:

SELECT TOP (10) 
[Project3].[Id] AS [Id], 
[Project3].[Name] AS [Name], 
[Project3].[C1] AS [C1]
FROM ( SELECT 
    [Project2].[Id] AS [Id], 
    [Project2].[Name] AS [Name], 
    [Project2].[C1] AS [C1]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        (SELECT 
            SUM([Join1].[A1]) AS [A1]
            FROM ( SELECT 
                CASE WHEN ([Project1].[C1] IS NULL) THEN cast(0 as decimal(18)) ELSE [Project1].[SubValueSum] END AS [A1]
                FROM   ( SELECT 1 AS X ) AS [SingleRowTable1]
                LEFT OUTER JOIN  (SELECT 
                    [Extent2].[SubValueSum] AS [SubValueSum], 
                    cast(1 as tinyint) AS [C1]
                    FROM [dbo].[Subvalue] AS [Extent2]
                    WHERE ([Extent1].[Id] = [Extent2].[Id]) AND ([Extent2].[Created] >= '2015-08-01') AND ([Extent2].[Created] <= '2015-10-01') AND ([Extent2].[StatusId] IN (1,2)) ) AS [Project1] ON 1 = 1
            )  AS [Join1]) AS [C1]
        FROM [dbo].[Project] AS [Extent1]
        WHERE ([Extent1].[ProjectCountryId] = 77) AND ([Extent1].[Active] = 1)
    )  AS [Project2]
)  AS [Project3]
ORDER BY [Project3].[C1] DESC;

EF 生成的查询的执行时间为 ~10 seconds 。但是当我像这样手动编写查询时:

select 
    TOP (10)
    Proj.Id,
    Proj.Name,
    SUM(Subv.SubValueSum) AS Total
from 
    SubValue as Subv
left join
    Project as Proj on Proj.Id = Subv.ProjectId
where
    Subv.Created > '2015-08-01' AND Subv.Created <= '2015-10-01' AND Subv.StatusId IN (1,2)
group by
    Proj.Id,
    Proj.Name
order by 
    Total DESC

执行时间接近即时;低于 30ms

问题显然在于我使用 LINQ 编写良好EF查询的能力,但无论我尝试做什么(使用 Linqpad 进行测试(,我都无法像手写的那样用LINQ'EF编写类似的高性能查询。我已经尝试查询子值表和项目表,但最终结果大致相同:多个无效的嵌套查询而不是单个联接来完成工作。

如何编写模仿上面所示手写SQL的查询?如何控制EF生成的实际查询?最重要的是:当我想要使用Joins而不是嵌套子查询时,我怎样才能Linq2SQLEntity Framework使用。

LINQ 和实体框架 - 避免子查询

EF 从您提供的 LINQ 表达式生成 SQL,您不能期望此转换完全解散您放入表达式的任何内容的结构以对其进行优化。在您的例子中,你已经创建了一个表达式树,对于每个项目,该表达式树将使用导航属性对与项目相关的一些子值求和。这会导致您发现的嵌套子查询。

要改进生成的 SQL,您需要避免在对子值执行所有操作之前从项目导航到子值,您可以通过创建连接来做到这一点(这也是您在手工制作的 SQL 中所做的(:

var query = from proj in context.Project
            join s in context.SubValue.Where(s => s.Created >= startDate && s.Created <= endDate && (s.StatusId == 1 || s.StatusId == 2)) on proj.Id equals s.ProjectId into s2
            from subv in s2.DefaultIfEmpty()
            select new { proj, subv } into x
            group x by new { x.proj.Id, x.proj.Name } into g
            select new {
              g.Key.Id,
              g.Key.Name,
              Total = g.Select(y => y.subv.SubValueSum).Sum()
            } into y
            orderby y.Total descending
            select y;
var result = query.Take(10);

基本思想是在受where子句限制的子值上联接项目。要执行左连接,您需要DefaultIfEmpty()但您已经知道这一点。

然后对连接值(x(进行分组,并在每个组中执行SubValueSum的求和。

最后,应用排序和TOP(10)

生成的 SQL 仍然包含子查询,但我希望它比查询生成的 SQL 更有效:

SELECT TOP (10)
    [Project1].[Id] AS [Id],
    [Project1].[Name] AS [Name],
    [Project1].[C1] AS [C1]
    FROM ( SELECT
        [GroupBy1].[A1] AS [C1],
        [GroupBy1].[K1] AS [Id],
        [GroupBy1].[K2] AS [Name]
        FROM ( SELECT
            [Extent1].[Id] AS [K1],
            [Extent1].[Name] AS [K2],
            SUM([Extent2].[SubValueSum]) AS [A1]
            FROM  [dbo].[Project] AS [Extent1]
            LEFT OUTER JOIN [dbo].[SubValue] AS [Extent2] ON ([Extent2].[Created] >= @p__linq__0) AND ([Extent2].[Created] <= @p__linq__1) AND ([Extent2].[StatusId] IN (1,2)) AND ([Extent1].[Id] = [Extent2].[ProjectId])
            GROUP BY [Extent1].[Id], [Extent1].[Name]
        )  AS [GroupBy1]
    )  AS [Project1]
    ORDER BY [Project1].[C1] DESC