Entitiy Framework v9 更新超时:从 linq 返回的记录数与更新时间之间的关系

问题描述 投票:0回答:1

我正在使用此代码来更新大约有 600k 记录的表的一些记录:

var someRecords = myContext.Commits.Where(c => c.ProjectName == "BSA" && c.RepoName == "sales-SalesApplication-Command" && c.CommitterUniqueName == null);

foreach (var commit in someRecords)
{
    commit.CommitterUniqueName = GetUniqueName(commit.CommitterName, myContext);
}

myContext.SaveChanges();

linq 返回 346 行,奇怪的是 30 秒后我收到超时错误。这很奇怪,因为使用 SQL Mgmt studio 的相同更新很快就会运行:

update Devex_Commit 
set CommitterUniqueName = 'abc'
where ProjectName = 'BSA'
and RepoName = 'sales-SalesApplication-Command'
and CommitterUniqueName is null

当我查看 EF 的调试日志时,我发现它使用 Id 字段来更新记录:

fail: 16.05.2024 14:17:56.737 RelationalEventId.CommandError[20102] (Microsoft.EntityFrameworkCore.Database.Command)
      Failed executing DbCommand (30,026ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      UPDATE [Devex_Commit] SET [CommitterUniqueName] = @p0
      OUTPUT 1
      WHERE [Id] = @p1;
fail: 16.05.2024 14:17:56.794 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update)
      An exception occurred in the database while saving changes for context type 'DevExDataJob.Models.DevexContext'.
      Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
       ---> Microsoft.Data.SqlClient.SqlException (0x80131904): Execution Timeout Expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.

当我稍微更改 linq 以仅返回一行时,一切正常:

var someRecords = myContext.Commits.Where(c => c.ProjectName == "BSA" && c.RepoName == "sales-SalesApplication-Command" && c.CommitterUniqueName == null && c.Id == 123);

foreach (var commit in someRecords)
{
    commit.CommitterUniqueName = GetUniqueName(commit.CommitterName, myContext);
}

myContext.SaveChanges();

本例中EF的调试日志没有变化:

info: 16.05.2024 13:48:42.897 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed         DbCommand (18ms    ) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      UPDATE [Devex_Commit] SET [CommitterUniqueName] = @p0
      OUTPUT 1
      WHERE [Id] = @p1;

当我调试返回 346 行的原始代码,并在仅更新一条记录后将执行从 foreach 循环中取出时,我仍然得到相同的超时!这让我认为从linq返回的记录数和更新操作所需的时间之间存在关系。我完全困惑了..

我不想增加 EF 的超时阈值,因为相同的更新查询会立即运行。

请指教。

c# entity-framework
1个回答
0
投票

the same update
这根本不是同一个更新。 EF 是一个 ORM,而不是嵌入式 SQL。主要概念是对象,而不是表。 ORM 将对各个对象的操作和更改映射到各个 SQL 命令。该问题的代码将所有 346 个对象加载到内存中并更改它们。当调用 SaveChanges 时,EF 会检测所有更改并为每个对象生成修改查询。

也就是说,346 个更新并不是很多。一定有其他东西阻止了

SaveChanges
执行的事务。其他一些低效查询或长期事务可能已在您要更新的行上获取了读(共享)锁。例如,在没有索引的情况下查询 600K 行将在查询运行时扫描(从而锁定)所有行。您可以使用 SSMS 活动监视器来查看哪个会话(连接)阻止您自己的会话以及它执行的内容。

问题的查询可能效率不高,即使过滤器列已建立索引,例如,如果大多数提交都在

BSA
项目中。如果不是,服务器将必须扫描整个表,从而锁定所有行。

可以使用 ExecuteUpdate 方法在 EF Core 7 及更高版本中执行等效的批量更新:

var query= myContext.Commits.Where(
    c => c.ProjectName == "BSA" 
    && c.RepoName == "sales-SalesApplication-Command" 
    && c.CommitterUniqueName == null);

query.ExecuteUpdate(setters => setters.SetProperty(
             b => b.CommitterUniqueName , 
             "abc"));

这会被转换为单个更新,相当于:

UPDATE Commits
SET CommitterUniqueName = 'abc'
WHERE
    ProjectName = 'BSA'
    AND RepoName = 'sales-SalesApplication-Command'
    AND CommitterUniqueName IS NULL

您可以使用表达式来计算新值,只要该表达式可以翻译为 SQL,例如:

query.ExecuteUpdate(setters => setters.SetProperty(
             c => c.CommitterUniqueName , 
             c => c.CommitterName + ":" + c.Id.ToString()));
© www.soinside.com 2019 - 2024. All rights reserved.