我的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
我在这里缺少什么?
答案 0 :(得分:24)
您不应该更新集合中的10k行,除非您确定该操作正在获取Page Locks(由于每页多行是UPDATE
操作的一部分)。问题是Lock Escalation(从Row或Page到Table锁定)发生在5000 locks 。所以最安全的是将它保持在5000以下,以防操作使用行锁。
您应该不使用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;
通过针对@Rows
测试@BatchSize
,您可以避免最终的UPDATE查询(在大多数情况下),因为最终集合通常是少于@BatchSize
的某些行数,在这种情况下我们知道没有更多要处理的内容(这是您在answer中显示的输出中看到的内容)。只有在最终行集等于@BatchSize
的情况下,此代码才会运行影响0行的最终UPDATE。
我还在WHERE
子句中添加了一个条件,以防止已经更新的行再次更新。
答案 1 :(得分:14)
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
END
答案 2 :(得分:3)
我想分享我的经验。几天前,我必须以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
任务:将人员更新为新职位:'软件开发人员' - &gt; '网络开发者'。
1。创建临时表'Persons_SoftwareDeveloper_To_WebDeveloper(Id INT主键)'
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,'软件开发人员' - &gt; '网络开发者'。
答案 3 :(得分:2)
你的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
代码。
答案 4 :(得分:1)
这是@Kramb解决方案的更有效版本。存在检查是多余的,因为update where子句已经处理了这个问题。取而代之的是,您只需获取行数并与batchsize进行比较。
还请注意,@ 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
答案 5 :(得分:0)
我昨天遇到了这个线程,并根据接受的答案编写了一个脚本。结果证明执行速度非常慢,需要 12 个小时来处理 33M 行中的 25M。今天早上我最终取消了它,并与 DBA 合作改进它。
DBA 指出我的 UPDATE 查询中的 is null
检查在 PK 上使用聚集索引 Scan,而正是扫描降低了查询速度。基本上,查询运行的时间越长,它就越需要通过索引查找正确的行。
事后看来,他提出的方法很明显。本质上,您将要更新的行的 ID 加载到临时表中,然后在更新语句中将其连接到目标表中。这使用索引 Seek 而不是扫描。伙计,它确实加快了速度!更新最后 800 万条记录用了 2 分钟。
SET NOCOUNT ON
DECLARE @Rows INT,
@BatchSize INT,
@Completed INT,
@Total INT
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 'Completed ' + cast(@Completed as varchar(10)) + '/' + cast(@Total as varchar(10))
-- 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
答案 6 :(得分:-2)
首先,感谢大家的投入。我调整了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)