我正在使用此代码来更新大约有 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 的超时阈值,因为相同的更新查询会立即运行。
请指教。
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()));