LINQ to SQL Func 作为输入性能

本文关键字:输入 性能 Func to SQL LINQ | 更新日期: 2023-09-27 18:32:55

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

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 with 1 用户 ID:0.0156318 - 有 ~100 个用户 Id: 0.145 5635

GetRewrite with 1 userId:0.4742711 - 有 ~100 个用户 Id: 7.2555701

正如你所看到的,差异是巨大的,任何人都可以分享为什么会发生这种情况?

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

LINQ to SQL Func 作为输入性能

看到性能差异非常大,我无法解释,因为它们应该运行完全相同。

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

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<T>上调用.Where时,它将(除非实现IQueryable<T>的类型具有自己的适用.Where这很奇怪(调用:

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

请记住,源代码中的 lambda 可以转换为Func<…>Expression<Func<…>>(如适用(。

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

不过,您的版本已将 lambda 显式分配给Func<ContactFeedback, bool>。因此,将其与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.ContactFeedbackDbContext.ContactFeedback.Where(predicate)DbContext.ContactFeedback.Where(predicate).GroupBy(x => new { TruncateTime = DbFunctions.TruncateTime(x.FeedbackDate), x.LeadId, x.UserId })如果枚举,都是查询,DbContext.ContactFeedback.Where(predicate).GroupBy(x => 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强制在 .NET 应用程序中执行Where和随后的所有内容,而不是在数据库上执行。