超级慢的 LINQ Max() 函数执行

Super slow LINQ Max() function execution

试图找出为什么 LINQ Max function 实际上比原始 SQL 查询慢得多。

var watch = Stopwatch.StartNew();
// LINQ Max function. This part takes around 2300 ms
ulong max = context.Messages.Select(m => m.Id).Max();
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds + "ms - Max");

watch.Restart();
// Raw SQL. This part takes 1 ms
var max2 = context.Messages.FromSql("SELECT * FROM messages WHERE id=(SELECT MAX(id) from messages)").ToList();
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds + "ms - MAX SQL");

结果(不是第一个 运行):

2308ms - Max
1ms - MAX SQL

编辑 - 分析器

我打开分析器,发现我有 this problem 代码:

var watch = Stopwatch.StartNew();
var maxId = context.Messages.Max(m => m.Id);
watch.Stop();
Console.WriteLine(watch.ElapsedMilliseconds + "ms - Max ");

因为我有这个警告:

Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'Max()' could not be translated and will be evaluated locally.

所以它执行了这个奇怪的 SQL 查询:

SELECT "m"."Id"
FROM "Messages" AS "m", 

Github 上面的问题说:

In linq/C# when you call aggregate methods like Average/Min/Max on an enumerable of non-nullable type which is empty, it throws exception.

因此我尝试用这个新代码解决问题

var maxId = context.Messages.Max(m => (ulong?)m.Id);

但仍然收到警告。我错过了什么?


EDIT2 - 备选方案

Quick fix/alternative 用于慢速 Max() 函数,不使用 raw-SQL 语句:

var message = context.Messages.OrderByDescending(m => m.Id).FirstOrDefault();

这个没有给出任何警告。

第一个语句 (Select) 没有对数据库执行任何操作,因此它应该是 0 毫秒。 Max() 等语句的行为因数据库适配器而异。

我想尝试一下:

var maxId = context.Messages.Max(m => m.Id);

为了比较:

var message1 = context.Messages.Where(m => m.Id == context.Messages.Max(x => x.Id)).Single();
//vs
var message2 = context.Messages.FromSql("SELECT * FROM messages WHERE id=(SELECT MAX(id) from messages)").Single();

如果这是针对 DbContext 的 "cold" 测试,我还希望在第一个 运行 上消除上下文的任何预热时间。我希望最初的 Select 调用会满足这一点,尽管 0ms 时间是可疑的,除非在该调用之前有东西触及 DbContext。可以肯定的是:

var warmup = context.Messages.First();

//start timing here...
var maxId = context.Messages.Select(m => m.Id).Max();
maxId = context.Messages.Max(m => m.Id);

var message1 = context.Messages.Where(m => m.Id = context.Messages.Max(x => x.Id)).Single();
//vs
var message2 = context.Messages.FromSql("SELECT * FROM messages WHERE id=(SELECT MAX(id) from messages)").Single();

从那里开始,如果最后两次调用之间存在差异,我将连接一个探查器(如果可能)并检查发送到数据库的查询。 EF 查询 (message1) 的主要区别在于它将显式列出实体中的列而不是 SELECT *.

编辑:好的,在对一个包含 300 万条记录的大型制造 table 进行一些实验之后。一些有趣的结果。 (运行 针对 SQL 服务器)

var stopWatch = Stopwatch.StartNew();
var query = context.Messages.Select(x => x.MessageId).Max();
stopWatch.Stop();
Console.WriteLine(stopWatch.ElapsedMilliseconds + "ms. (Select.Max)" + "  " + query);
stopWatch.Restart();

query = context.Messages.Select(x => x.MessageId).Max(x=>x);
stopWatch.Stop();
Console.WriteLine(stopWatch.ElapsedMilliseconds + "ms. (Select.Max(x=>))" + "  " + query);
stopWatch.Restart();

query = context.Messages.Max(x => x.MessageId);
stopWatch.Stop();
Console.WriteLine(stopWatch.ElapsedMilliseconds + "ms. (Max(x=>))" + "  " + query);
stopWatch.Restart();

var result = context.Messages.Where(x => x.MessageId == context.Messages.Max(y => y.MessageId)).Single();
stopWatch.Stop();
Console.WriteLine(stopWatch.ElapsedMilliseconds + "ms. (get by Max(x=>))"  + "  " + result.MessageId);

此净值结果为:

257ms. (Select.Max)  3000000
4ms. (Select.Max(x=>))  3000000
3ms. (Max(x=>))  3000000
210ms. (get by Max(x=>))  3000000

有趣的结果是: .Select(x => x.MessageId).Max(); 对比 .Select(x => x.MessageId).Max(x => x);

第一个用了 257 毫秒,第二个用了 4 毫秒。 然而,逆向查询看到的结果是一样的。 .Select().Max(x => x) 风格花费了 257 毫秒,而 Select.Max() 花费了 4 毫秒。

运行针对 运行 的分析器表明所有查询甚至都没有记录执行成本。每个 0 毫秒。所以这个时间被EF内部占用了。

奇怪的是,前三个场景得到的最大ID运行完全一样SQL..

SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
        MAX([Extent1].[MessageId]) AS [A1]
        FROM [dbo].[Messages] AS [Extent1]
    )  AS [GroupBy1]
go

将 运行 分离到单独的 DbContext 实例中并没有立即产生影响。第一个 运行 再次受到性能影响,随后的 运行s.

通过将使用 Max() 选择实体的查询移动为第一个查询 运行 产生了更有趣的结果:

486ms. (get by Max(x=>))  3000000
50ms. (Max(x=>))  3000000
14ms. (Select.Max(x=>))  3000000
2ms. (Select.Max)  3000000

它首先 运行 明显较慢,但以下查询速度很快,没有 200 毫秒 运行s,但不像 "fast" 单独那样。

我尝试更深入地研究: .OrderByDescending(x => x.MessageId).Select(x => x.MessageId).Take(1).Single()

首先执行它的执行时间为 100 毫秒,而 .Max() 的执行时间为 230+ 毫秒,而在 Max() 之后执行时,执行时间约为 50 毫秒。

从长远来看,关闭延迟加载和代理创建没有任何效果。使用 .AsNoTracking() 对查询时间也几乎没有影响。

鉴于 SQL 与 运行 相同,虽然执行时间的这种差异无法真正解释,并且查询执行时间不能解释任何差异,但很明显 EF 正在做一些超出幕后热身。在我针对 SQL 服务器的测试用例中,执行时间差异很明显,但不足以解释在 SQLite 中看到的情况。我怀疑 SQLite 适配器可能会引入更昂贵的查询,因此我将研究分析选项以确认正在执行的查询。

"Trying to figure out why the LINQ Max function is actually way slower than the raw SQL query."

默认情况下,LINQ 函数将检索 整个 table,然后在客户端中进行处理。您添加的网络负载等于 1 个结果集乘以 table 中的行数。从磁盘检索、网络传输和内存分配将是您的瓶颈。

SQL 将在数据库中进行处理。它有索引之类的东西来加速这个过程。所以它甚至可能不需要查询磁盘。也许它甚至缓存了以前的最大值。然后只传输一个值。无论 DBMS 处理速度有多快,都将成为瓶颈。我毫不怀疑它很快。数据库专为两件事而设计:

  1. 真实性
  2. 速度

我的一般建议是:

  • 如果您需要进行任何过滤,对我们的分页进行排序(包括像 MAX 这样的基数运算符)- 总是 在 DBMS 中进行。始终从数据库中检索尽可能少的值。
  • 但也不要向数据库发送垃圾邮件单个请求。每个查询都有开销,如果你能用一个
  • 得到相同的结果