【问题标题】:Entity Framework Core 3.0 query causes "SqlException: 'Execution Timeout Expired'" and tempdb become full. Works with EF Core 2.2.6Entity Framework Core 3.0 查询导致“SqlException: 'Execution Timeout Expired'”并且 tempdb 变满。适用于 EF Core 2.2.6
【发布时间】:2020-02-09 09:57:07
【问题描述】:

我在 Microsoft Entity Framework Core 3.0 中运行一个相当简单的查询,如下所示:

var dbProfile = db.Profiles.Where(x => x.SiteId == Int32.Parse(id))
    .Include(x => x.Interests)
    .Include(x => x.Pets)
    .Include(x => x.Networks)
    .Include(x => x.PersonalityTraits)
    .SingleOrDefault();

它在 EF Core 2.2.6 上运行良好,但在升级到 EF Core 3.0 时,此查询会立即针对 721 个配置文件运行,但对于至少一个配置文件,查询会超时:

Microsoft.Data.SqlClient.SqlException: '执行超时已过期。
在操作完成之前超时时间已过或服务器没有响应。'

然后我记录了发送到数据库服务器的实际查询:

https://stackoverflow.com/a/58348159/3850405

SELECT [t].[Id], [t].[Age], [t].[City], [t].[Country], [t].[County], [t].[DeactivatedAccount], [t].[Gender], [t].[HasPictures], [t].[LastLogin], [t].[MemberSince], [t].[PresentationUpdated], [t].[ProfileName], [t].[ProfilePictureUrl], [t].[ProfileText], [t].[SiteId], [t].[VisitorsCount], [i].[Id], [i].[Name], [i].[ProfileId], [p0].[Id], [p0].[Description], [p0].[Name], [p0].[ProfileId], [n].[Id], [n].[Name], [n].[NetworkId], [n].[ProfileId], [p1].[Id], [p1].[Name], [p1].[ProfileId]
FROM (
    SELECT TOP(2) [p].[Id], [p].[Age], [p].[City], [p].[Country], [p].[County], [p].[DeactivatedAccount], [p].[Gender], [p].[HasPictures], [p].[LastLogin], [p].[MemberSince], [p].[PresentationUpdated], [p].[ProfileName], [p].[ProfilePictureUrl], [p].[ProfileText], [p].[SiteId], [p].[VisitorsCount]
    FROM [Profiles] AS [p]
    WHERE ([p].[SiteId] = '123') AND '123' IS NOT NULL
) AS [t]
LEFT JOIN [Interests] AS [i] ON [t].[Id] = [i].[ProfileId]
LEFT JOIN [Pets] AS [p0] ON [t].[Id] = [p0].[ProfileId]
LEFT JOIN [Networks] AS [n] ON [t].[Id] = [n].[ProfileId]
LEFT JOIN [PersonalityTraits] AS [p1] ON [t].[Id] = [p1].[ProfileId]
ORDER BY [t].[Id], [i].[Id], [p0].[Id], [n].[Id], [p1].[Id]

然后我尝试在 SSMS 中运行实际的 SQL,结果出现以下错误:

消息 1105,第 17 级,状态 2,第 1 行
无法为数据库“tempdb”中的对象“dbo.SORT 临时运行存储:140737692565504”分配空间,因为“PRIMARY”文件组已满。通过删除不需要的文件、删除文件组中的对象、向文件组中添加其他文件或为文件组中的现有文件设置自动增长来创建磁盘空间。

我的tempdb 现在已经完全填满了数据库磁盘。我已经尝试了 10 个其他 ID,并且相同的查询立即运行。

我尝试使用命令DBCC SHRINKDATABASE(tempdb, 10); 再次缩小 tempdb,它工作正常。但是,当我再次尝试运行查询时,同样的事情发生了。如果我跳过包括表格,一切都很好。这里可能有什么问题,我该如何解决?这是 EF Core 3.0 中的已知错误吗?查看 EF Core 2.2.6 中的查询,它对所有表执行像这样的单独选择:

SELECT [x.Interests].[Id], [x.Interests].[Name], [x.Interests].[ProfileId]
FROM [Interests] AS [x.Interests]
INNER JOIN (
    SELECT TOP(1) [x0].[Id]
    FROM [Profiles] AS [x0]
    WHERE [x0].[SiteId] = '123'
    ORDER BY [x0].[Id]
) AS [t] ON [x.Interests].[ProfileId] = [t].[Id]
ORDER BY [t].[Id]

【问题讨论】:

  • 除非数据量很大,否则此查询应该可以正常运行。 tempdb 爆炸通常表明表没有正确索引。您是否对所有外键字段和SiteId 都有索引?此外,运行统计数据可能会产生巨大的影响。

标签: c# sql-server entity-framework entity-framework-core entity-framework-core-3.0


【解决方案1】:

这是 EF Core 3 中记录的重大更改:Eager loading of related entities now happens in a single query

新行为类似于 EF6 中的查询生成,其中多次包含会创建非常大且成本高昂的查询。这些查询也可能由于超时、查询计划生成成本或查询执行资源耗尽而失败。

因此,就像在 EF6 中一样,您需要避免包含多个不相关的实体包含路径,因为这些会创建非常昂贵的查询。

相反,您可以使用延迟加载,或在单独的查询中显式加载实体图的一部分,并让更改跟踪器修复导航属性。

EF 5 添加了一个选项来关闭名为Split Queries 的大查询生成。

【讨论】:

  • 我们似乎偶然发现了同样的问题。我只是想确认它是否是同一个问题。从 2.2 升级到 3.1 后,我们还会遇到查询超时,但似乎并非每次都会发生这种情况。 19/20 查询将在 100 毫秒内运行,而 1/20 将运行 5 分钟并且永远不会完成。这就是这件事的奇怪之处。我可以理解性能下降,但我不明白的是缺乏一致性。
  • 可能是。这在创建多个包含分支的多个包含中最为常见。但是您需要对服务器上的查询执行进行概要分析才能确定。
【解决方案2】:

对@DavidBrowne-Microsoft 答案的补充。

假设我们在模型中有以下实体和导航

Customer
Customer.Address (reference nav)
Customer.Orders (collection nav)
Order.OrderDetails (collection nav)
Order.OrderDiscount (reference nav)

您只需要为集合导航重写,引用导航可以是同一查询的一部分。

var baseQuery = db.Customers.Include(c => c.Address).Where(c => c.CustomerName == "John");
var result = baseQuery.ToList(); // Or async method, If doing FirstOrDefault, add Take(1) to base query
baseQuery.Include(c => c.Orders).ThenInclude(o => o.OrderDiscount).SelectMany(c => c.Orders).Load();
baseQuery.SelectMany(c => c.Orders).SelectMany(o => o.OrderDetails).Load();

这将向服务器生成 3 个查询。它会比 EF Core 2.2 生成的 SQL 稍微优化一些。 StateManager 将修复导航。它还避免了将来自服务器的任何记录复制到客户端。

生成的 SQL:

// Customer Include Address
SELECT [c].[Id], [c].[CustomerName], [a].[Id], [a].[City], [a].[CustomerId]
FROM [Customers] AS [c]
LEFT JOIN [Address] AS [a] ON [c].[Id] = [a].[CustomerId]
WHERE ([c].[CustomerName] = N'John') AND [c].[CustomerName] IS NOT NULL

// Order Include Order discount
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o0].[Id], [o0].[Discount], [o0].[OrderId]
FROM [Customers] AS [c]
INNER JOIN [Order] AS [o] ON [c].[Id] = [o].[CustomerId]
LEFT JOIN [OrderDiscount] AS [o0] ON [o].[Id] = [o0].[OrderId]
WHERE ([c].[CustomerName] = N'John') AND [c].[CustomerName] IS NOT NULL

// OrderDetails
SELECT [o0].[Id], [o0].[OrderId], [o0].[ProductName]
FROM [Customers] AS [c]
INNER JOIN [Order] AS [o] ON [c].[Id] = [o].[CustomerId]
INNER JOIN [OrderDetail] AS [o0] ON [o].[Id] = [o0].[OrderId]
WHERE ([c].[CustomerName] = N'John') AND [c].[CustomerName] IS NOT NULL

编辑:如果您没有在 SelectMany 中使用 CLR 导航,那么您可以使用 EF.Property 来引用集合导航。

来源:https://github.com/aspnet/EntityFrameworkCore/issues/18022#issuecomment-537219137

【讨论】:

    【解决方案3】:

    您需要像这样更改您的代码,然后它将生成更多查询并避免超时

    var dbProfile = db.Profiles.SingleOrDefault(x => x.SiteId == Int32.Parse(id));
    dbProfile.Include(x => x.Interests).Load();
    dbProfile.Include(x => x.Pets).Load();
    dbProfile.Include(x => x.Networks).Load();
    dbProfile.Include(x => x.PersonalityTraits).Load();
    

    记住查询应该在跟踪模式下执行。 如果它不加载孩子,你可以像这样添加 asTracking:

    db.Profiles.AsTracking().Where(........
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-25
      • 2020-09-11
      • 1970-01-01
      相关资源
      最近更新 更多