【问题标题】:Remove duplicated SUM subquery in Entity Framework Core query删除 Entity Framework Core 查询中重复的 SUM 子查询
【发布时间】:2021-09-02 11:34:48
【问题描述】:

我有以下 LINQ 代码:

return from policy in db.Policy.Include(it => it.LedgerLines)
       let balance = policy.LedgerLines.Sum(it => it.Amount)
       where balance > 0m && balance < 5m
       select policy;

这被翻译成

SELECT ...
FROM [Policy] AS [p]
LEFT JOIN [PolicyLedger] AS [p0] ON [p].[Id] = [p0].[PolicyId]
WHERE (((SELECT SUM([p1].[Amount])
         FROM [PolicyLedger] AS [p1]
         WHERE [p].[Id] = [p1].[PolicyId]) > 0.0)) 
   AND ((SELECT SUM([p2].[Amount])
         FROM [PolicyLedger] AS [p2]
         WHERE [p].[Id] = [p2].[PolicyId]) < 5.0)
ORDER BY [p].[Id], [p0].[Id]

有没有办法只执行一次SUM([p1].[Amount]) 子查询?

(EF 核心 3.1)

【问题讨论】:

  • 你能从LedgerLine实体开始吗?
  • 有趣的想法,@Progman - 我尝试从分类帐行开始并按策略 ID 分组,但我从 EF 核心收到一个错误,说它无法对分组部分做些什么。另外,我不想为了删除子查询而做一个非常晦涩的查询——我宁愿让它慢一点也不要太复杂。
  • 如果删除Include会发生什么
  • @Charlieface 不多——SQL 发生了一些变化(删除了 JOIN),但两个 SUM 子查询仍然存在。

标签: c# linq entity-framework-core


【解决方案1】:

线

let balance = policy.LedgerLines.Sum(it => it.Amount)

相当于中间投影,清楚地表明了重用表达式的意图。

但是 EF Core 查询翻译器通过尽可能地消除子查询,付出了很多努力来产生“漂亮”的查询。不幸的是,在这种情况下,在这方面似乎做得太多了。

话虽如此,您可以将其视为翻译缺陷,让 LINQ 查询“保持原样”并等待改进的翻译 - EFC 5.x 没有改进,可能是 EFC 6.0 或更高版本,如果永远。

但这里有一个不那么令人分心的技巧,让 EFC 3.1 / 5.x 生成 JOINGROUP BY 子查询并重用 SUM 表达式。

对原始 LINQ 查询的唯一更改是将上面的 let 语句替换为以下内容


from balance in policy.LedgerLines
    .GroupBy(it => it.PolicyId)
    .Select(g => g.Sum(it => it.Amount))

被翻译成

SELECT ...
FROM [Policy] AS [p]
INNER JOIN (
    SELECT SUM([p0].[Amount]) AS [c], [p0].[PolicyId]
    FROM [PolicyLedger] AS [p0]
    GROUP BY [p0].[PolicyId]
) AS [t] ON [p].[Id] = [t].[PolicyId]
LEFT JOIN [PolicyLedger] AS [p1] ON [p].[Id] = [p1].[PolicyId]
WHERE ([t].[c] > 0.0) AND ([t].[c] < 5.0)
ORDER BY [p].[Id], [p1].[Id]

【讨论】:

  • 哇,这是一个绝妙的技巧,谢谢你——我将进一步思考它的作用是否足够清楚(我可能需要添加一些 cmets),但它确实可以我的要求。
【解决方案2】:

您可以从LedgerLine 实体开始查询,并使用GroupBy() 为每个策略构建Amount 列的总和。但是,您不能在导航属性上进行分组,因此您必须改为在 PolicyId 上进行分组。这意味着您需要在之后将 PolicyId 列与 Policies 表/DbSet 连接以获取实际的 Policy 实体(具有任何必需的包含集合属性)。

代码可能如下所示:

var result = context.LedgerLines
                .Include(it => it.Policy)
                .GroupBy(it => it.PolicyId)
                .Select(it => new {
                    policyId = it.Key,
                    sum = it.Sum(a => a.Amount)
                })
                .Join(context.Policies.Include(it => it.LedgerLines),
                    it => it.policyId,
                    it => it.Id,
                    (a,b) => new {
                        a.sum,
                        policy=b
                    })
                .Where(it => it.sum > 0m && it.sum < 5m)
                .Select(it => it.policy)
                .ToList();

这将生成这样的查询(对于 MySQL):

SELECT `p`.`Id`, `p`.`Name`, `l0`.`Id`, `l0`.`Amount`, `l0`.`PolicyId`
FROM (
    SELECT `l`.`PolicyId`, SUM(`l`.`Amount`) AS `c`
    FROM `LedgerLines` AS `l`
    GROUP BY `l`.`PolicyId`
) AS `t`
INNER JOIN `Policies` AS `p` ON `t`.`PolicyId` = `p`.`Id`
LEFT JOIN `LedgerLines` AS `l0` ON `p`.`Id` = `l0`.`PolicyId`
WHERE (CAST(`t`.`c` AS decimal(18, 2)) > 0) AND (CAST(`t`.`c` AS decimal(18, 2)) < 5)
ORDER BY `p`.`Id`, `l0`.`Id`

正如您所见,只使用了一个 SUM() 调用,但我不确定您 JOIN 两次在 LedgerLines 表上的性能,更不用说这段代码看起来很奇怪而且很麻烦。

【讨论】:

  • 你是绝对正确的 - 我明天会接受这个作为答案,如果没有更好的结果,但我会保持原样,因为我认为结果肯定比理解更复杂原始的(可能也更慢)。
  • 在想那个,缺点是你需要join和groupby。但它看起来确实更有效率。我会像这样删除额外的Include .Join(context.Policies,
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-25
  • 2020-09-11
  • 1970-01-01
  • 2020-10-27
  • 1970-01-01
  • 1970-01-01
  • 2017-03-19
相关资源
最近更新 更多