【问题标题】:Why is Entity Framework generating a JOIN on SELECT为什么实体框架会在 SELECT 上生成 JOIN
【发布时间】:2020-01-23 14:10:24
【问题描述】:

我在 C# 应用程序中使用实体框架,并且正在使用延迟加载。我们注意到一个查询对我们的 CPU 有非常大的影响,它只是计算一个总和。在调试实体框架生成的查询时,它会创建一个不高效的INNER JOIN (SELECT ...。当我手动将查询更改为正确的 JOIN 时,查询时间从 1.3 秒变为 0.03 秒。

让我用我的代码的简化版本来说明它。

public decimal GetPortfolioValue(Guid portfolioId)
{
   var value = DbContext.Portfolios
        .Where( x => x.Id.Equals(portfolioId) )
        .SelectMany( p => p.Items
            .Where( i => i.Status == ItemStatusConstants.Subscribed 
                && _activeStatuses.Contains( i.Category.Status ) )
        )
        .Select( i => i.Amount )
        .DefaultIfEmpty(0)
        .Sum();

   return value;
}

这会生成一个查询,该查询选择总和,但对连接在一起的两个表的 SELECT 执行内部连接。我为生成的查询创建了一个 pastebin here,以免污染这个问题,但缩短的版本是:

SELECT ...
FROM `portfolios` AS `Extent1`
INNER JOIN (SELECT 
               `Extent2`.*,
               `Extent3`.*
            FROM `items` AS `Extent2`
            INNER JOIN `categories` AS `Extent3` ON `Extent3`.`id` = 
`Extent2`.`category_id`) AS `Join1`
ON `Extent1`.`id` = `Join1`.`portfolio_id`
    AND ((`Join1`.`status` = @gp1)
    AND (`Join1`.`STATUS1` IN (@gp2, @gp3, @gp4, @gp5, @gp6)))
WHERE ...

我希望它生成的查询(需要 0.03 秒而不是 1.3 秒)类似于

SELECT ...
FROM `portfolios` AS `Extent1`
INNER JOIN `items` AS `Extent2` ON `Extent2`.`portfolio_id` = `Extent1`.`id`
INNER JOIN `categories` AS `Extent3` ON `Extent3`.`id` = `Extent2`.`category_id`
    AND ((`Extent2`.`status` = @gp1)
    AND (`Extent3`.`status` IN (@gp2, @gp3, @gp4, @gp5, @gp6)))
WHERE ...

我怀疑这是由于.SelectMany 造成的,但我不知道应该如何重写 LINQ 查询以提高效率。至于实体,链接属性是虚拟的,并且配置了外键:

public class Portfolio
{
   public Guid Id { get; set; }
   public virtual ICollection<Item> Items { get; set; }
}

public class Item
{
   public Guid Id { get; set; }
   public Guid PortfolioId { get; set; }
   public Guid CategoryId { get; set; }
   public decimal Amount { get; set; }
   public string Status { get; set; }
   public virtual Portfolio Portfolio { get; set; }
   public virtual Category Category { get; set; }
}

public class Category
{
   public Guid Id { get; set; }
   public string Status { get; set; }
   public virtual ICollection<Item> Items { get; set; }
}

任何帮助将不胜感激!

【问题讨论】:

  • 这些只是 JOIN。 服务器 应该能够生成不加载这些字段的执行计划。这两个查询的执行计划是什么?您使用的是哪个 MySQL 版本?查询性能由执行计划控制,而执行计划又取决于查询数据统计信息。而 MySQL 5.7.x 版本即使在次要版本之间也存在显着差异
  • SQL 查询由数据库提供程序生成,而不是 EF。您使用了哪个提供商?柚?还有什么?还是连接器/网络?如果您使用的是 Connector/NET,您是否尝试过使用其他东西,例如 Pomelo?
  • 也许您可以尝试稍微更改一下您的查询。假设portfolioId 是唯一的,您可以像这样更改您的查询: var value = DbContext.Portfolios .First( x => x.Id.Equals(portfolioId) ).Items...
  • it creates a INNER JOIN (SELECT ... which is not performant. 为什么?除非查询优化器未能简化查询,否则这相当于只是将所有表连接在一起。如果服务器在这方面发生故障,那是服务器错误。查询中的 真的很奇怪 构造是 (SELECT 1 AS 'X') AS 'SingleRowTable1' LEFT OUTER JOIN ...ON 1=1 。这可能是一些在以后的版本中适得其反的未记录技巧吗?
  • 再次,您使用的是哪个提供商?如果Item 不是实体,则提供商本身应该抱怨。它不应该发出基于反射的 SQL 查询。它太聪明了

标签: c# mysql performance entity-framework linq


【解决方案1】:

由于您不需要 Portfolio 中的任何内容,只需按 PortfolioId 过滤,您可以直接查询 PortfolioItems。假设您的 DbContext 有一个包含所有投资组合中所有项目的 DbSet,可能是这样的:

var value = DbContext.PortfolioItems
                     .Where(i => i.PortfolioId == portfolioId && i.Status == ItemStatusConstants.Subscribed && _activeStatuses.Contains(i.Category.Status))
                     .Sum(i=>i.Amount);                 

如果您直接使用适当的 Queryable.Sum 重载,我相信您不需要 DefaultIfEmpty 或 select。

已编辑:在不公开 DbSet 的情况下尝试了两种不同的 LINQ 查询。

第一个查询和你的基本一样:

var value2 = dbContext.Portfolios
    .Where(p => p.Id == portfolioId)
    .SelectMany(p => p.Items)
    .Where(i => i.Status == "A" && _activeStatuses.Contains(i.Category.Status))
    .Select(i=>i.Amount)
    .DefaultIfEmpty()
    .Sum();

在 SQL Server 中分析查询(手头没有 MySql)并产生一个丑陋的句子(参数被替换并且引号未转义以进行测试):

SELECT [GroupBy1].[a1] AS [C1] 
FROM   (SELECT Sum([Join2].[a1_0]) AS [A1] 
    FROM   (SELECT CASE 
                     WHEN ( [Project1].[c1] IS NULL ) THEN Cast( 
                     0 AS DECIMAL(18)) 
                     ELSE [Project1].[amount] 
                   END AS [A1_0] 
            FROM   (SELECT 1 AS X) AS [SingleRowTable1] 
                   LEFT OUTER JOIN 
                   (SELECT [Extent1].[amount] AS [Amount], 
                           Cast(1 AS TINYINT) AS [C1] 
                    FROM   [dbo].[items] AS [Extent1] 
                           INNER JOIN [dbo].[categories] AS 
                                      [Extent2] 
                                   ON [Extent1].[categoryid] = 
                                      [Extent2].[id] 
                    WHERE  ( N'A' = [Extent1].[status] ) 
                           AND ( [Extent1].[portfolioid] = 
                                 'E2CC0CC2-066F-45C9-9D48-543D92C4C92E' ) 
                           AND ( [Extent2].[status] IN ( N'A', N'B', N'C' ) 
                               ) 
                           AND ( [Extent2].[status] IS NOT NULL )) AS 
                   [Project1] 
                                ON 1 = 1) AS [Join2]) AS [GroupBy1] 

如果我们删除“Select”和“DefaultIfEmpty”方法,并将查询重写为:

var value = dbContext.Portfolios
    .Where(p => p.Id == portfolioId)
    .SelectMany(p => p.Items)
    .Where(i => i.Status == "A" && _activeStatuses.Contains(i.Category.Status))
    .Sum(i => i.Amount);

生成的句子更干净:

SELECT [GroupBy1].[a1] AS [C1] 
FROM   (SELECT Sum([Extent1].[amount]) AS [A1] 
    FROM   [dbo].[items] AS [Extent1] 
           INNER JOIN [dbo].[categories] AS [Extent2] 
                   ON [Extent1].[categoryid] = [Extent2].[id] 
    WHERE  ( N'A' = [Extent1].[status] ) 
           AND ( [Extent1].[portfolioid] = 
                 'E2CC0CC2-066F-45C9-9D48-543D92C4C92E' ) 
           AND ( [Extent2].[status] IN ( N'A', N'B', N'C' ) ) 
           AND ( [Extent2].[status] IS NOT NULL )) AS [GroupBy1] 

结论:我们不能依赖 LINQ 提供程序来创建优化查询。 linq 查询必须在生成 SQL 语句之前进行分析和优化。

【讨论】:

  • 是的,但项目未在 DbSet 上公开。我不想暴露它,因为它们不应该单独存在。我想强制代码/开发人员通过 Portfolio。
  • @Jules 然后使用这个: var value = DbContext.Portfolio.First(p => p.Id == portfolioId).Items.Where(i => i.Status == ItemStatusConstants.Subscribed && _activeStatuses.Contains(i.Category.Status)) .Sum(i=>i.Amount);
  • but the Items are not exposed on the DbSet 这是什么意思? EF(或提供者)无法为非实体生成查询。您使用的是哪个提供商?无论如何,如果您只想查询项目,请将它们添加为 DbSet
  • 我认为@PanagiotisKanavos 是对的,添加 DbSet。此外,如果您的 DbContext 中只有顶级实体的 DbSet,则 LINQ 查询可能会变得不必要的复杂。要查询顶级实体以下五个级别的集合,您必须遍历之前的四个级别。这乘以相当数量的查询可能会让事情变得混乱。
  • 我的意思是我没有项目的 DbSet 属性。我不需要那个。永远不会直接选择一个项目,因为它在我的功能用例中没有意义。只应检索属于指定投资组合的项目。所以你总是需要通过投资组合来检索项目。我同意如果你有 5 个级别,你可以走捷径,但这里不是这样。丑化你的代码应该是你最后的手段。
猜你喜欢
  • 2011-09-02
  • 1970-01-01
  • 1970-01-01
  • 2020-05-13
  • 2012-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多