EF核心:简单查询-为什么这么慢?

时间:2020-05-20 07:30:22

标签: sql-server entity-framework-core

EF核心版本:3.1。

这是我的方法:

public static ILookup<string, int> GetClientCountLookup(DepotContext context, DateRange dateRange)
            => context
                .Flows
                .Where(e => e.TimeCreated >= dateRange.Start.Date && e.TimeCreated <= dateRange.End.Date)
                .GroupBy(e => e.Customer)
                .Select(g => new { g.Key, Count = g.Count() })
                .ToLookup(k => k.Key, e => e.Count);

所有使用的字段均已建立索引。

此处生成的查询:

SELECT [f].[Customer] AS [Key], COUNT(*) AS [Count]
FROM [Flows] AS [f]
WHERE ([f].[TimeCreated] >= @__dateRange_Start_Date_0) AND ([f].[TimeCreated] <= @__dateRange_End_Date_1)
GROUP BY [f].[Customer]

以SQL执行该查询时,执行时间为100毫秒。 通过ToLookup方法在代码中使用该查询时-执行时间为3200ms。

更奇怪的是-EF Core中的执行时间似乎完全与数据样本大小无关(也就是说,根据我们可以计算成百上千个记录的日期范围而定)。

这里到底发生了什么?

我粘贴的查询是EF Core发送的真实查询。 我首先粘贴的代码片段在3200毫秒内执行。 然后,我采用了精确生成的SQL,并在Visual Studio中以SQL查询的形式执行-花了100毫秒。

对我来说没有任何意义。我使用EF Core已有很长时间了,它似乎表现合理。 大多数查询(简单,简单,没有日期范围)都很快,结果可立即获取(不到200毫秒)。

在我的应用程序中,我构建了一个真正的庞大查询,该查询具有4个多列联接和子查询...猜猜是什么-它在3200毫秒内获取400行。它还在3200毫秒内获取4000行。而且,当我删除大多数联接(包括)时,甚至删除了子查询-3200ms。还是4000,具体取决于我的Internet或服务器的瞬时状态和负载。

这就像持续滞后,我将其精确定位到我粘贴的第一个查询。

我知道ToLookup方法会导致最终获取所有输入表达式结果,但就我而言(真实世界的数据)-恰好有5行。

结果如下:

|------------|-------|
| Key        | Count |
|------------|-------|
| Customer 1 | 500   |
| Customer 2 | 50    |
| Customer 3 | 10    |
| Customer 4 | 5     |
| Customer 5 | 1     |

从数据库中获取5行需要4秒?!太荒谬了如果获取了整个表,则对行进行分组并计数-这将加起来。但是生成的查询实际上返回了5行。

这里发生了什么,我想念什么?

请不要要求我提供完整代码。它是机密的,是我客户项目的一部分,不允许我透露客户的商业秘密。不在这里,也没有任何其他问题。我知道当您没有我的数据库和整个应用程序时,很难理解会发生什么,但是这里的问题是纯理论上的。您要么知道发生了什么,要么您不知道。就如此容易。但是,这个问题很难。

我只能说所使用的RDBMS是在Ubuntu服务器上远程运行的MS SQL Express。测量的时间是在我的AMD Ryzen 7 8核心3.40GHz处理器上执行代码测试(NUnit)或对远程数据库进行查询的时间。该服务器位于Azure上,例如2核I5 2.4GHz或类似的东西。

1 个答案:

答案 0 :(得分:2)

这是测试:

[Test]
public void Clients() {
    var dateRange = new DateRange {
        Start = new DateTime(2020, 04, 06),
        End = new DateTime(2020, 04, 11)
    };
    var q1 = DataContext.Flows;
    var q2 = DataContext.Flows
        .Where(e => e.TimeCreated >= dateRange.Start.Date && e.TimeCreated <= dateRange.End.Date)
        .GroupBy(e => e.Customer)
        .Select(g => new { g.Key, Count = g.Count() });
    var q3 = DataContext.Flows;
    var t0 = DateTime.Now;
    var x = q1.Any();
    var t1 = DateTime.Now - t0;
    t0 = DateTime.Now;
    var l = q2.ToLookup(g => g.Key, g => g.Count);
    var t2 = DateTime.Now - t0;
    t0 = DateTime.Now;
    var y = q3.Any();
    var t3 = DateTime.Now - t0;
    TestContext.Out.WriteLine($"t1 = {t1}");
    TestContext.Out.WriteLine($"t2 = {t2}");
    TestContext.Out.WriteLine($"t3 = {t3}");
}

这是测试结果:

t1 = 00:00:00.6217045 // the time of dummy query
t2 = 00:00:00.1471722 // the time of grouping query
t3 = 00:00:00.0382940 // the time of another dummy query

是的:147ms是我的分组,以前需要3200ms。 发生了什么?之前执行过虚拟查询。

这解释了为什么结果几乎不取决于数据样本大小!

巨大的无法解释的时间是INITIALIZATION,而不是实际的查询时间。我的意思是,如果不是以前的伪查询,整个时间将花费在ToLookup行代码上!该行将初始化DbContext,创建与数据库的连接,然后执行实际查询并获取数据。

作为最后的答案,我可以说我的测试方法是错误的。我测量了对我的DbContext进行第一次查询的时间。这是错误的,应该在测量时间之前初始化数据库。我可以通过在测量查询之前执行任何查询来做到这一点。

好吧,另一个问题出现了-为什么第一个查询这么慢,为什么初始化这么慢。如果我的Blazor应用将DbContext用作Transient(每次注入都被实例化)-每次会花费这么多时间吗?我不这么认为,因为这是我的应用程序在进行重大重新设计之前的工作方式。它没有明显的延迟(在页面之间切换时,我会注意到3秒钟的延迟)。但是我不确定。现在,我的应用程序使用范围为DbContext的应用程序,因此它是一个用于用户会话的应用程序。因此,我根本看不到初始化开销,所以-在虚拟查询之后测量时间的方法似乎是准确的。