【问题标题】:LINQ and Entity Framework - Avoiding subqueriesLINQ 和实体框架 - 避免子查询
【发布时间】:2015-09-23 10:55:30
【问题描述】:

我很难在我的应用程序中调整我的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 而不是嵌套子查询时使用。

【问题讨论】:

  • 使用 LINQ 你很可能使用DataContext。看看DataContext.LoadOptions 属性here..
  • 设置 context.ObjectTrackingEnabled = false,你会得到很大的提升。
  • @JanUnld 感谢您的建议,但我不确定它对这个特定问题有何帮助?能详细解释一下吗?
  • @Nikita 问题未跟踪。问题是 EF 生成的查询非常无效,我想知道如何更好地控制 EF/ling2sql 生成的实际 SQL。那么我怎样才能让它使用连接而不是无效的子查询呢?
  • 实体框架和 LINQ To SQL 不一样。生成的 SQL 看起来像是由实体框架生成的。如果这是正确的,我建议您删除 [linq-to-sql] 标记并添加 [entity-framework] 标记。

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


【解决方案1】:

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 的总和。

最后,ordering 和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

【讨论】:

  • 嗯,这是一个非常好的答案,并解释了很多关于 EF 如何实际生成 SQL 的事情。非常感谢!生成的 SQL 确实运行得更快 :)
  • 请注意,LINQ to SQL 生成的 SQL 比 EF 高效得多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 1970-01-01
  • 2015-04-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多