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

时间:2015-09-23 10:55:31

标签: c# sql-server entity-framework linq

我很难在应用程序中调优' --part of my query here' is not a valid identifier生成的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。我查询了 SubValue 表和 Project 表,但结果大致相同:多个无效的嵌套子查询,而不是单个联接完成工作。

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

1 个答案:

答案 0 :(得分:5)

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