【问题标题】:LINQ to SQL Func as input PerformanceLINQ to SQL Func 作为输入性能
【发布时间】:2014-12-02 23:42:15
【问题描述】:

我创建了一个使用 EF 6 的简单方法,该方法将根据一些输入信息和一些可能的 Type 和 SubType 值进行分组查询,如下所示

public int GetOriginal(DateTime startDate, DateTime endDate, List<int> userIds)
{
    DateTime dt = DateTime.UtcNow;
    var ret = DbContext.ContactFeedback
           .Where(c => c.FeedbackDate >= startDate && 
            c.FeedbackDate <= endDate && userIds.Contains(c.UserId) &&
            (c.Type == FeedbackType.A || c.Type == FeedbackType.B || c.Type == FeedbackType.C))
            .GroupBy(x => new {TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId})
            .Count();
    Console.WriteLine(string.Format("{0}",DateTime.UtcNow - dt));
    return ret;
}

它按预期工作,但是如果我尝试创建一个新的辅助方法,该方法接收“查询”(Func 类型对象)作为要运行的输入,我会发现性能差异很大,我无法做到解释一下,因为它们应该运行完全相同。 这是我重写的方法

public int GetRewritten(DateTime startDate, DateTime endDate, List<int> userIds)
{
    DateTime dt = DateTime.UtcNow;
    var query = new Func<ContactFeedback, bool>(c => c.FeedbackDate >= startDate && c.FeedbackDate <= endDate && userIds.Contains(c.UserId) &&
                 (c.Type == FeedbackType.A || c.Type == FeedbackType.B ||
                  c.Type == FeedbackType.C));
    var ret = GetTotalLeadsByFeedback(query);
    Console.WriteLine(string.Format("{0}",DateTime.UtcNow - dt));
    return ret;
}

private int GetTotalLeadsByFeedback(Func<ContactFeedback, bool> query)
{
    return DbContext.ContactFeedback
        .Where(query)
        .GroupBy(x => new { TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId })
        .Count();
}

这里是以秒为单位的运行时间

GetOriginal 有 1 个用户 ID:0.0156318 - 有大约 100 个用户 ID:0.1455635

GetRewritten 使用 1 个用户 ID:0.4742711 - 使用约 100 个用户 ID:7.2555701

您可以看到差异很大,任何人都可以分享一下为什么会发生这种情况?

如果有帮助,我将使用 SQL Server DB 在 Azure 上运行所有内容

【问题讨论】:

标签: c# linq entity-framework linq-to-sql entity-framework-6


【解决方案1】:

我发现性能差异很大,我无法解释,因为它们的运行方式应该完全相同。

它们的方法有很大不同。初始方法查询的第一部分:

DbContext.ContactFeedback
       .Where(c => c.FeedbackDate >= startDate && 
        c.FeedbackDate <= endDate && userIds.Contains(c.UserId) &&
        (c.Type == FeedbackType.A || c.Type == FeedbackType.B || c.Type == FeedbackType.C))

相当于:

DbContext.ContactFeedback
      .Where(new Expression<Func<ContactFeedback, bool>>(new Func<ContactFeedback, bool>(c => c.FeedbackDate >= startDate && c.FeedbackDate <= endDate && userIds.Contains(c.UserId) &&
             (c.Type == FeedbackType.A || c.Type == FeedbackType.B ||
              c.Type == FeedbackType.C)))

当您在IQueryable&lt;T&gt; 上调用.Where 时,它会调用(除非实现IQueryable&lt;T&gt; 的类型有自己适用的.Where,这会很奇怪)调用:

public static IQueryable<TSource> Where<TSource>(
  this IQueryable<TSource> source,
  Expression<Func<TSource, bool>> predicate
)

请记住,源代码中的 lambda 可以转换为 Func&lt;…&gt;Expression&lt;Func&lt;…&gt;&gt;(如果适用)。

Entity Framework 然后将此查询与GroupBy 组合,最后在Count() 将整个查询转换为适当的SELECT COUNT … 查询,由数据库执行(取决于表内容和设置的索引的速度,但应该相当快),然后从数据库发回单个值供 EF 获取。

尽管您的版本已将 lambda 显式分配给 Func&lt;ContactFeedback, bool&gt;。因此将它与Where 一起使用它必须调用:

public static IEnumerable<TSource> Where<TSource>(
  this IEnumerable<TSource> source,
  Func<TSource, bool> predicate
)

所以要做到Where EF 必须从数据库中检索每一行的 每一 列,然后过滤掉那些 Func 返回 true 的行,然后将它们分组在执行Count 之前,内存(需要存储部分构造的组),通过以下机制:

public int Count<T>(this IEnumerable<T> source)
{
  /* some attempts at optimising that don't apply to this case and so in fact just waste a tiny amount omitted */
  int tally = 0;
  using(var en = source.GetEnumerator())
    while(en.MoveNext())
      ++tally;
  return tally;
}

这需要更多的工作,因为 EF 和数据库之间的流量更多,因此速度会慢很多。

您尝试的那种重写将更好地近似为:

public int GetRewritten(DateTime startDate, DateTime endDate, List<int> userIds)
{
    DateTime dt = DateTime.UtcNow;
    var query = new Expression<Func<ContactFeedback, bool>>(c => c.FeedbackDate >= startDate && c.FeedbackDate <= endDate && userIds.Contains(c.UserId) &&
                 (c.Type == FeedbackType.A || c.Type == FeedbackType.B ||
                  c.Type == FeedbackType.C));
    var ret = GetTotalLeadsByFeedback(query);
    Console.WriteLine(string.Format("{0}",DateTime.UtcNow - dt));
    return ret;
}

private int GetTotalLeadsByFeedback(Expression<Func<ContactFeedback, bool>> predicate)
{
    return DbContext.ContactFeedback
        .Where(predicate)
        .GroupBy(x => new { TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId })
        .Count();
}

(还请注意,我将谓词的名称更改为predicate,因为predicate 更常用于谓词,query 用于源以及作用于它的零个或多个方法;所以DbContext.ContactFeedback , DbContext.ContactFeedback.Where(predicate)DbContext.ContactFeedback.Where(predicate).GroupBy(x =&gt; new { TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId }) 如果枚举,都将是查询,DbContext.ContactFeedback.Where(predicate).GroupBy(x =&gt; new { TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId }).Count() 是立即执行并返回单个值的查询)。

相反,您最终得到的表单可以写回GetOriginal 的样式为:

public int GetNotOriginal(DateTime startDate, DateTime endDate, List<int> userIds)
{
    DateTime dt = DateTime.UtcNow;
    var ret = DbContext.ContactFeedback
           .AsEnumerable()
           .Where(c => c.FeedbackDate >= startDate && 
            c.FeedbackDate <= endDate && userIds.Contains(c.UserId) &&
            (c.Type == FeedbackType.A || c.Type == FeedbackType.B || c.Type == FeedbackType.C))
            .GroupBy(x => new {TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId})
            .Count();
    Console.WriteLine(string.Format("{0}",DateTime.UtcNow - dt));
    return ret;
}

注意AsEnumerable 强制Where 以及随后的所有内容都在.NET 应用程序中执行,而不是在数据库中执行。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-16
    • 2011-06-08
    • 2010-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多