如何在 SQL Server 中更新具有数百万行的大表?
Posted
技术标签:
【中文标题】如何在 SQL Server 中更新具有数百万行的大表?【英文标题】:How to update large table with millions of rows in SQL Server? 【发布时间】:2016-03-09 21:52:21 【问题描述】:我有一个UPDATE
语句,它可以更新超过百万条记录。我想分批更新它们 1000 或 10000。我尝试使用 @@ROWCOUNT
,但无法获得所需的结果。
出于测试目的,我选择了包含 14 条记录的表并将行数设置为 5。此查询应该更新 5、5 和 4 中的记录,但它只更新前 5 条记录。
查询 - 1:
SET ROWCOUNT 5
UPDATE TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123
WHILE @@ROWCOUNT > 0
BEGIN
SET rowcount 5
UPDATE TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123
PRINT (@@ROWCOUNT)
END
SET rowcount 0
查询 - 2:
SET ROWCOUNT 5
WHILE (@@ROWCOUNT > 0)
BEGIN
BEGIN TRANSACTION
UPDATE TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123
PRINT (@@ROWCOUNT)
IF @@ROWCOUNT = 0
BEGIN
COMMIT TRANSACTION
BREAK
END
COMMIT TRANSACTION
END
SET ROWCOUNT 0
我在这里错过了什么?
【问题讨论】:
query2 有什么问题? 不要像这样使用 ROWCOUNT。它已被弃用。 msdn.microsoft.com/en-us/library/ms188774.aspx @JuanCarlosOropeza 命令成功完成,但实际上没有任何记录更新。 所有答案都在一遍又一遍地更新相同的记录。您需要区分更新的记录和未触及的记录。我认为这就是问题所在。也许您想在 where 子句中添加“AND Value 'abc1'” PRINT 语句破坏逻辑,删除该行。 【参考方案1】:除非您确定该操作正在获得页面锁定,否则您不应更新一组中的 10k 行(因为每页多行是 UPDATE
操作的一部分)。问题是锁升级(从行锁或页锁到表锁)发生在 5000 个锁。所以将其保持在 5000 以下是最安全的,以防万一操作使用了 Row Locks。
您应该不使用SET ROWCOUNT 来限制将被修改的行数。这里有两个问题:
自 SQL Server 2005 发布(11 年前)以来,它已被弃用:
使用 SET ROWCOUNT 不会影响 SQL Server 未来版本中的 DELETE、INSERT 和 UPDATE 语句。避免在新的开发工作中将 SET ROWCOUNT 与 DELETE、INSERT 和 UPDATE 语句一起使用,并计划修改当前使用它的应用程序。对于类似的行为,请使用 TOP 语法
它不仅会影响您正在处理的语句:
设置 SET ROWCOUNT 选项会导致大多数 Transact-SQL 语句在受到指定行数的影响时停止处理。这包括触发器。 ROWCOUNT 选项不影响动态游标,但它会限制键集和不敏感游标的行集。应谨慎使用此选项。
改为使用TOP ()
子句。
在这里进行显式事务没有任何目的。它使代码复杂化,您无需处理 ROLLBACK
,甚至不需要处理,因为每个语句都是其自己的事务(即自动提交)。
假设您找到了保留显式事务的理由,那么您没有 TRY
/ CATCH
结构。有关处理事务的TRY
/ CATCH
模板,请参阅我在 DBA.StackExchange 上的回答:
Are we required to handle Transaction in C# Code as well as in Store procedure
我怀疑问题中的示例代码中没有显示真正的WHERE
子句,因此仅依靠已显示的内容,更好模型(请参阅注释下面关于性能)将是:
DECLARE @Rows INT,
@BatchSize INT; -- keep below 5000 to be safe
SET @BatchSize = 2000;
SET @Rows = @BatchSize; -- initialize just to enter the loop
BEGIN TRY
WHILE (@Rows = @BatchSize)
BEGIN
UPDATE TOP (@BatchSize) tab
SET tab.Value = 'abc1'
FROM TableName tab
WHERE tab.Parameter1 = 'abc'
AND tab.Parameter2 = 123
AND tab.Value <> 'abc1' COLLATE Latin1_General_100_BIN2;
-- Use a binary Collation (ending in _BIN2, not _BIN) to make sure
-- that you don't skip differences that compare the same due to
-- insensitivity of case, accent, etc, or linguistic equivalence.
SET @Rows = @@ROWCOUNT;
END;
END TRY
BEGIN CATCH
RAISERROR(stuff);
RETURN;
END CATCH;
通过针对@BatchSize
测试@Rows
,您可以避免最终的UPDATE
查询(在大多数情况下),因为最终集合通常比@BatchSize
少一些行数,在这种情况下我们知道有不再需要处理(这是您在answer 中显示的输出中看到的内容)。只有在最终行集等于@BatchSize
的情况下,此代码才会运行最终的UPDATE
,影响0 行。
我还在WHERE
子句中添加了一个条件,以防止已经更新的行再次被更新。
关于性能的说明
我在上面强调了“更好”(例如,“这是一个 更好 模型”),因为这对 O.P. 的原始代码有一些改进,并且在许多情况下都可以正常工作,但是并不适合所有情况。对于至少有一定大小的表(由于几个因素而变化,所以我不能更具体),性能会下降,因为需要修复的行更少:
-
没有支持查询的索引,或者
有索引,但是
WHERE
子句中至少有一列是不使用二进制排序规则的字符串数据类型,因此这里的查询中增加了COLLATE
子句强制二进制排序规则,这样做会使索引无效(对于此特定查询)。
这是@mikesigs 遇到的情况,因此需要不同的方法。更新的方法将所有要更新的行的 ID 复制到一个临时表中,然后使用该临时表到 INNER JOIN
到在聚集索引键列上更新的表。 (捕获和连接 聚集索引 列很重要,无论这些列是否是主键列!)。
详情请见下方@mikesigs answer。该答案中显示的方法是一种非常有效的模式,我自己在很多场合都使用过。我要做的唯一改变是:
-
显式创建
#targetIds
表而不是使用SELECT INTO...
对于#targetIds
表,在列上声明一个聚集主键。
对于#batchIds
表,在列上声明一个聚集主键。
要插入#targetIds
,请使用INSERT INTO #targetIds (column_name(s)) SELECT
并删除ORDER BY
,因为它是不必要的。
因此,如果您没有可用于此操作的索引,并且无法临时创建一个可以实际工作的索引(过滤索引可能有效,具体取决于您的 WHERE
子句 @987654353 @query),然后尝试@mikesigs answer 中显示的方法(如果您使用该解决方案,请投票)。
【讨论】:
如果我不更新一列而是假设更新 10 列怎么办?我是否必须比较所有列的值?什么是最高效的方法? @asemprini87 尽可能多地进行比较,以减少不必要的更新,因为它们需要更长的时间并更多地增加日志文件。我刚刚更新了我的答案,包括为Value
过滤器强制使用二进制排序规则,但是您可以在任何其他字符串列上使用COLLATE Latin1_General_100_BIN2
来加速字符串匹配,假设您只是在寻找完全匹配并且不需要考虑大小写差异等。我正在考虑为此操作创建过滤索引,但如果必须为每个批次更新它可能不会更快。不过可能值得测试。
@SolomonRutzky :说在更新数百万条记录时,如果很少有记录失败,那么您将如何记录那些未能更新的记录。示例。在 100 万条记录中,5 百万条记录成功更新,接下来的 5 条记录更新失败,其余记录更新成功,那么你将如何从一百万条记录中记录这 5 条失败的记录。
@RockingDev 要检测单个错误行,您需要在循环中添加额外的逻辑来处理较小的批量大小,直到您到达出错的行,然后我猜将该键值放入临时表,然后将批量大小重置回原始值。这需要先创建临时表,然后通过AND NOT EXISTS (SELECT * FROM #ErrorRows err WHERE err.KeyVal = tab.ClusteredIndexKey)
将其添加到WHERE
子句中。类似的东西。处理完成后,#ErrorRows 中的任何行都需要处理。【参考方案2】:
WHILE EXISTS (SELECT * FROM TableName WHERE Value <> 'abc1' AND Parameter1 = 'abc' AND Parameter2 = 123)
BEGIN
UPDATE TOP (1000) TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123 AND Value <> 'abc1'
END
【讨论】:
我已经将它从 1000 更新到了 4000,到目前为止似乎工作正常。在一张表中,我更新了 500 万条记录(似乎每 10 分钟更新约 744,000 条记录)。我在开发服务器上运行它,接下来将尝试更新 2600 万条记录。目前正在寻找是否有办法通过“多线程”加快进程。 这是低效的,因为存在检查是多余的。相反,您可以在运行 UPDATE 后检索 @@ROWCOUNT,如果 @@ROWCOUNT BatchSize 则您已完成并可以退出循环。 @Shiv@@ROWCOUNT
是一个全局变量。 USE、SET
@Kramb 我知道 - 您可以根据答案缓存行数 ***.com/a/55054293/1519839【参考方案3】:
我昨天遇到了这个线程,并根据接受的答案编写了一个脚本。结果证明执行速度非常慢,需要 12 个小时来处理 33M 行中的 25M。我今天早上取消了它,并与 DBA 一起改进它。
DBA 指出,我的 UPDATE 查询中的 is null
检查在 PK 上使用了聚集索引 Scan,正是扫描使查询速度变慢。基本上,查询运行的时间越长,它就越需要在索引中查找正确的行。
事后看来,他想出的方法是显而易见的。本质上,您将要更新的行的 ID 加载到临时表中,然后在更新语句中将其连接到目标表中。这使用索引 Seek 而不是扫描。天哪,它确实加快了速度!更新最后 8M 条记录需要 2 分钟。
使用临时表进行批处理
SET NOCOUNT ON
DECLARE @Rows INT,
@BatchSize INT,
@Completed INT,
@Total INT,
@Message nvarchar(max)
SET @BatchSize = 4000
SET @Rows = @BatchSize
SET @Completed = 0
-- #targetIds table holds the IDs of ALL the rows you want to update
SELECT Id into #targetIds
FROM TheTable
WHERE Foo IS NULL
ORDER BY Id
-- Used for printing out the progress
SELECT @Total = @@ROWCOUNT
-- #batchIds table holds just the records updated in the current batch
CREATE TABLE #batchIds (Id UNIQUEIDENTIFIER);
-- Loop until #targetIds is empty
WHILE EXISTS (SELECT 1 FROM #targetIds)
BEGIN
-- Remove a batch of rows from the top of #targetIds and put them into #batchIds
DELETE TOP (@BatchSize)
FROM #targetIds
OUTPUT deleted.Id INTO #batchIds
-- Update TheTable data
UPDATE t
SET Foo = 'bar'
FROM TheTable t
JOIN #batchIds tmp ON t.Id = tmp.Id
WHERE t.Foo IS NULL
-- Get the # of rows updated
SET @Rows = @@ROWCOUNT
-- Increment our @Completed counter, for progress display purposes
SET @Completed = @Completed + @Rows
-- Print progress using RAISERROR to avoid SQL buffering issue
SELECT @Message = 'Completed ' + cast(@Completed as varchar(10)) + '/' + cast(@Total as varchar(10))
RAISERROR(@Message, 0, 1) WITH NOWAIT
-- Quick operation to delete all the rows from our batch table
TRUNCATE TABLE #batchIds;
END
-- Clean up
DROP TABLE IF EXISTS #batchIds;
DROP TABLE IF EXISTS #targetIds;
批处理慢的方式,不要用!
作为参考,这里是原始执行速度较慢的查询:
SET NOCOUNT ON
DECLARE @Rows INT,
@BatchSize INT,
@Completed INT,
@Total INT
SET @BatchSize = 4000
SET @Rows = @BatchSize
SET @Completed = 0
SELECT @Total = COUNT(*) FROM TheTable WHERE Foo IS NULL
WHILE (@Rows = @BatchSize)
BEGIN
UPDATE t
SET Foo = 'bar'
FROM TheTable t
JOIN #batchIds tmp ON t.Id = tmp.Id
WHERE t.Foo IS NULL
SET @Rows = @@ROWCOUNT
SET @Completed = @Completed + @Rows
PRINT 'Completed ' + cast(@Completed as varchar(10)) + '/' + cast(@Total as varchar(10))
END
【讨论】:
+1 我同意这是一种非常有效的方法,人们应该尝试一下。如果他们遇到像您在我的方法中遇到的性能问题,我会在此处更新我对直接读者的回答。我以前使用过这种模式,所以也许我专注于解决 O.P. 方法中的缺陷。我确实有一些关于对您的方法进行细微更改的建议,我在答案的末尾详细说明了这些建议。我没有提到的一件事是简化输出,您可以通过以下方式完成:DECLARE @Completed INT = 5, @Total INT = 37; RAISERROR('Completed %d / %d', 10, 1, @Completed, @Total) WITH NOWAIT;
【参考方案4】:
我想分享我的经验。几天前,我必须用 7600 万条记录更新表中的 2100 万条记录。我的同事建议了下一个变体。 例如,我们有下一个表“Persons”:
Id | FirstName | LastName | Email | JobTitle
1 | John | Doe | abc1@abc.com | Software Developer
2 | John1 | Doe1 | abc2@abc.com | Software Developer
3 | John2 | Doe2 | abc3@abc.com | Web Designer
任务:将人员更新为新职位:“软件开发人员”->“Web 开发人员”。
1.创建临时表“Persons_SoftwareDeveloper_To_WebDeveloper (Id INT Primary Key)”
2. 选择您要使用新职位更新的临时表人员:
INSERT INTO Persons_SoftwareDeveloper_To_WebDeveloper SELECT Id FROM
Persons WITH(NOLOCK) --avoid lock
WHERE JobTitle = 'Software Developer'
OPTION(MAXDOP 1) -- use only one core
取决于行数,此语句将需要一些时间来填充您的临时表,但它会避免锁定。在我的情况下,大约需要 5 分钟(2100 万行)。
3.主要思想是生成微sql语句来更新数据库。所以,让我们打印它们:
DECLARE @i INT, @pagesize INT, @totalPersons INT
SET @i=0
SET @pagesize=2000
SELECT @totalPersons = MAX(Id) FROM Persons
while @i<= @totalPersons
begin
Print '
UPDATE persons
SET persons.JobTitle = ''ASP.NET Developer''
FROM Persons_SoftwareDeveloper_To_WebDeveloper tmp
JOIN Persons persons ON tmp.Id = persons.Id
where persons.Id between '+cast(@i as varchar(20)) +' and '+cast(@i+@pagesize as varchar(20)) +'
PRINT ''Page ' + cast((@i / @pageSize) as varchar(20)) + ' of ' + cast(@totalPersons/@pageSize as varchar(20))+'
GO
'
set @i=@i+@pagesize
end
执行此脚本后,您将收到数百个批处理,您可以在 MS SQL Management Studio 的一个选项卡中执行这些批处理。
4. 运行打印的 sql 语句并检查表上的锁。您始终可以停止进程并使用 @pageSize 来加快或减慢更新速度(不要忘记在暂停脚本后更改 @i)。
5. 删除 Persons_SoftwareDeveloper_To_AspNetDeveloper。删除临时表。
次要注意:此迁移可能需要一些时间,并且在迁移过程中可能会插入包含无效数据的新行。因此,首先修复行添加的位置。在我的情况下,我修复了 UI,“软件开发人员”->“Web 开发人员”。
【讨论】:
【参考方案5】:这是@Kramb 解决方案的更高效版本。存在检查是多余的,因为 update where 子句已经处理了这个问题。相反,您只需获取行数并与批量大小进行比较。
另请注意@Kramb 解决方案没有从下一次迭代中过滤掉已经更新的行,因此它将是一个无限循环。
还使用现代批量大小语法而不是使用行数。
DECLARE @batchSize INT, @rowsUpdated INT
SET @batchSize = 1000;
SET @rowsUpdated = @batchSize; -- Initialise for the while loop entry
WHILE (@batchSize = @rowsUpdated)
BEGIN
UPDATE TOP (@batchSize) TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123 and Value <> 'abc1';
SET @rowsUpdated = @@ROWCOUNT;
END
【讨论】:
循环不会是无限的,因为它使用与过滤相同的参数更新结果集。因此,以下结果集不会包含之前更新的行。 @Kramb 您正在更新 Value 并查看 Parameter1 和 Parameter2。所以不,你没有过滤你实际更新的字段。我添加了第三个过滤器术语来检查您在答案中缺少的 Value 'abc1'。 再试一次...我的回答清楚地表明,我的Exists
查询中的第一个条件实际上是检查Value
是否等于内部查询设置的值@ 987654324@到。
@Kramb 如果更新查询中的前 1000 行已设置值,但前 1000 行之外的行未设置值,则您的解决方案将无限循环。您的解决方案存在缺陷,因为您缺少对实际 UPDATE 调用的 where 检查。存在检查不是问题。【参考方案6】:
您的print
搞砸了,因为它重置了@@ROWCOUNT
。每当您使用@@ROWCOUNT
时,我的建议是始终 立即将其设置为变量。所以:
DECLARE @RC int;
WHILE @RC > 0 or @RC IS NULL
BEGIN
SET rowcount 5;
UPDATE TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123 AND Value <> 'abc1';
SET @RC = @@ROWCOUNT;
PRINT(@@ROWCOUNT)
END;
SET rowcount = 0;
而且,另一个不错的功能是您无需重复 update
代码。
【讨论】:
查看我上面发布的关于使用 ROWCOUNT 控制更新行的链接。 @Gordon 我使用这个逻辑并且查询运行了 2 分钟(仅适用于 14 条记录!!!)。它进入无限循环。 @CSharper 。 . .嗯,如果没有更新行,那么@@ROWCOUNT
应该是 0,而不是 NULL。无限循环的原因并不明显。 “印刷品”在生产什么?如果update
产生NULL
,则可以通过将@RC
设置为某个任意值然后从WHILE
中删除@RC IS NULL
条件来解决此问题。
@GordonLinoff print 无限生成(5 row(s) affected) 1
。
@CSharper 。 . . where
子句需要排除已更新的行。【参考方案7】:
首先,感谢大家的投入。我调整了我的Query - 1
并得到了我想要的结果。 Gordon Linoff 是对的,PRINT
搞砸了我的查询,所以我将其修改如下:
修改后的查询 - 1:
SET ROWCOUNT 5
WHILE (1 = 1)
BEGIN
BEGIN TRANSACTION
UPDATE TableName
SET Value = 'abc1'
WHERE Parameter1 = 'abc' AND Parameter2 = 123
IF @@ROWCOUNT = 0
BEGIN
COMMIT TRANSACTION
BREAK
END
COMMIT TRANSACTION
END
SET ROWCOUNT 0
输出:
(5 row(s) affected)
(5 row(s) affected)
(4 row(s) affected)
(0 row(s) affected)
【讨论】:
以上是关于如何在 SQL Server 中更新具有数百万行的大表?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 pandas 或 python 将具有数百万行的表从 PostgreSQL 复制到 Amazon Redshift