【问题标题】:Why does "IQueryable.Where()" provide different results than "IEnumerable.Where()" in this query?为什么“IQueryable.Where()”在此查询中提供与“IEnumerable.Where()”不同的结果?
【发布时间】:2019-09-11 18:59:51
【问题描述】:

我正在尝试使用 linq 从 EF Core 数据集中过滤一些行,但我不明白为什么过滤 IEnumerable 和过滤 IQueryable 会给我不同的结果。

        var query = _db.Jobs
            .IsDelivered()
            .Include(a => a.JobExtras)
            .Include(a => a.Tips)
            .Include(a => a.Payments)
            .HasPayments();

        var query1 = query
            .ToList()
            .Where(a => a.Payments.Sum(b => b.Amount)
                         - a.Price.Value
                         - a.Discount
                         - a.JobExtras.Sum(c => c.Price)
                         - a.Tips.Sum(d => d.Amount)
                         > 0);

        var query2 = query
           .Where(a => a.Payments.Sum(b => b.Amount)
                         - a.Price.Value
                         - a.Discount
                         - a.JobExtras.Sum(c => c.Price)
                         - a.Tips.Sum(d => d.Amount)
                         > 0);

        Debug.WriteLine($"Record Count[Before Where Clause]: {query.Count()}");
        Debug.WriteLine($"Record Count[ToList() version]: {query1.Count()}");
        Debug.WriteLine($"Record Count[w/out ToList()]: {query2.Count()}");

这是输出:

Record Count[Before Where Clause]: 8379
Record Count[ToList() version]: 5921
Record Count[w/out ToList()]: 0

为什么 IEnumerable 版本产生 5921 条记录,而 IQueryable 版本产生 0 条记录?

【问题讨论】:

  • 您计算中的这些列中是否有NULLs? NULL 上的任何数学或逻辑都会产生 NULL,包括 NULL > 0 -> NULL 这不是真的,所以它在功能上等同于假。
  • 我想也许也是这样,所以我删除了唯一可以为空的“价格”但结果相同的术语。另外,如果是这样的话,那不应该像 query2 一样影响 query1 吗?
  • 没有。当您调用 ToList 时,它会执行查询,此时您正在对 .NET 类型/值进行操作。 Sum 将忽略 null 而不是产生 nullCount 将计算 null 项目而不是忽略它。如果您的源数据没有 NULLs,那将没有实际意义,但我认为当您没有要聚合的项目时,您可能会收到 NULL

标签: c# entity-framework linq entity-framework-core


【解决方案1】:

第一个查询使用 .NET 类型和 Enumerable.Sum() 在内存中执行。传递到Sum 的任何空值都将被跳过,因此您的一些数据正在通过> 0 测试。

第二个查询在数据库中执行并使用 SQL 的SUM。如果Where 子句中的表达式的任何部分包含空值,则该表达式将评估为空值。获取该表达式并对其进行比较 (null > 0) 在 SQL 中的计算结果始终为 false,因此您的所有行都会被过滤。

您的第二个查询可能是可以修复的。我没有证据证明这一点,因为我不知道您的模型(即什么可以为空),并且不确定 EF 是否可以翻译:

.Where(a => a.Payments.Where(x => x != null).Sum(b => b.Amount)
     - a.Price.Value
     - a.Discount
     - a.JobExtras.Where(x => x != null).Sum(c => c.Price)
     - a.Tips.Where(x => x != null).Sum(d => d.Amount)
     > 0);

【讨论】:

  • 感谢您的思路。 madreflection 正确地提到了 NULL 作为问题,但不是正确的区域。我认为问题不是可空字段,而是可空集。 Iqueryable 是 SQL,而 SQL 处理“集合”,而不是对象。因此,当您尝试执行 Tips.Sum(blah blah) 时,如果没有提示行(其中有几个 Job 对象没有“子”提示行),SQL 返回“NULL”,而 .NET 返回一个“ 0' 使整个查询无法通过 SQL 使用。想法?
  • 如果我在上一条评论中是正确的,这意味着您尝试的“修复”也将不起作用,因为 Tips.Where(x => x!= null) 仍然会导致 NULL 如果存在是数据库中的 0 个提示行。
  • 我将此标记为正确答案,因为它让我找到了解决方案。我用最终的工作解决方案在下面创建了一个答案。感谢大家的帮助。
  • @FrankBusalacchi:FWIW,当我说“没有要聚合的项目”时,我在第二条评论中提到了集合
  • @madreflection -- 是的,我同意你也提到了它,你也是第一个回应的人!这就是为什么我在回答中说基于你们俩提供的信息。这也是我赞成您的评论的原因。感谢您对我的问题的帮助和努力!感谢您并感谢所有花时间阅读并提供想法、建议和解决方案的人。
【解决方案2】:

好的,有几件事要尝试,但我读到您发现问题在于失踪的孩子。

我进行了快速测试,以下应该适用于您的情况:

  var query2 = query
       .Where(a => a.Payments.Sum(b => b.Amount)
                     - a.Price.Value
                     - a.Discount
                     - (a.JobExtras.Any() ? a.JobExtras.Sum(c => c.Price) : 0)
                     - (a.Tips.Any() ? a.Tips.Sum(d => d.Amount) : 0)
                     > 0);

【讨论】:

    【解决方案3】:

    所以根据Kit和madreflection提供的信息:

    我刚刚重新植入数据库并确保 3 个子表中有行:Payments、JobExtras 和 Tips。

    瞧,查询适用于 IQueryable 版本和 IEnumerable 版本。我得到相同的行数。

    因此,总而言之,问题的答案是,IQueryable 查询的问题是,如果数据库中没有我要求和的对象的子行,SQL 将返回 NULL 而不是0

    感谢您的帮助!

    这种行为是一个有趣的问题。

    那么现在,有没有办法修复 IQueryable 查询以应对子对象(Tips、Payments、JobExtras)可能是“空集”(无行)的可能性?

    ============== 可行的解决方案(感谢 @Steve Py 和 @madreflection)

    var query2 = query
       .Where(a => (a.Payments.Any() ? a.Payments.Sum(b => b.Amount) : 0M)
                   - (a.Price ?? 0M)
                   - a.Discount
                   - (a.JobExtras.Any() ? a.JobExtras.Sum(c => c.Price) : 0M)
                   - (a.Tips.Any() ? a.Tips.Sum(d => d.Amount) : 0M)
                   > 0M);
    
    // Even better solution [cleaner SQL generated] provided by: @madreflection
    
       .Where(a => (a.Payments.Sum(b => (decimal?)b.Amount) ?? 0M)
                   - (a.Price ?? 0M)
                   - a.Discount
                   - (a.JobExtras.Sum(c => (decimal?)c.Price) ?? 0M)
                   - (a.Tips.Sum(d => (decimal?)d.Amount) ?? 0M)
                   > 0M);
    
    

    【讨论】:

    • 你能改用a.Payments.Sum(b => (decimal?)b.Amount) ?? 0M)吗?
    • 正如 Kit 所暗示的,它归结为 IQueryable.Sum() 在呈现空集合时的行为。当集合为空时,IQueryable.Sum() = NULL。 IEnumerable.Sum() = 0 当集合为空时。 SQL 纯粹主义者可能会认为这是正确的,但在我看来,IQueryable.Sum() 和 IEnumerable.Sum 应该具有一致的行为,并且要么都返回 Null,要么都返回 0。
    • 我明白了;这一切都说得通。我只是想简化你的表达方式。 Sum 的结果并不总是 0。对于可空值类型,它是null,您可以将其合并到0。将b.Amount(例如)转换为可为空的将消除对Any 的调用以及三元运算符的需要。理论上——你必须对其进行测试才能确认——它能够生成更好的 SQL。
    • @madreflection:我刚刚交换了你的建议,它也确实有效。我绝对更喜欢查看三元组的方式,虽然我还没有调查过,但我同意你的观点,即生成的 SQL 更适合你的方法。我将更新我的答案并提供替代方案,以防其他人将来阅读此内容。感谢您一直以来的努力。
    • 我很乐意提供帮助。
    【解决方案4】:

    考虑到 Jobs.Price 作为查询中唯一可以为空的值,那么 Iqueryable linq 查询可以重写如下:

    .Where(a => a.Payments.Sum(b => b.Amount)
         - a.Price.Value
         - a.Discount
         - a.JobExtras.Sum(c => c.Price ?? 0)
         - a.Tips.Sum(d => d.Amount)
         > 0);
    

    【讨论】:

    • 所有对象中唯一可以为空的字段是 Jobs.Price。试图做 b => Amount ?? 0 甚至不会编译,因为 Amount 不可为空。不知何故,当数据库中没有 Payment/Tips/JobExtras 行时,表达式 a.JobExtras.Sum(c=>c.Price) 必须计算为 0 而不是 NULL
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-11
    • 1970-01-01
    • 1970-01-01
    • 2021-06-02
    • 2021-01-22
    相关资源
    最近更新 更多