如何在SQL Server中更新包含数百万行的大表?

CSh*_*per 19 sql-server

我的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
Run Code Online (Sandbox Code Playgroud)

查询 - 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
Run Code Online (Sandbox Code Playgroud)

我在这里错过了什么?

Sol*_*zky 29

  1. 您不应该更新集合中的10k行,除非您确定操作正在获取页面锁定(由于每页多行是UPDATE操作的一部分).问题是锁定升级(从行或页面到表锁定)发生在5000个锁定.因此,最好将其保持在5000以下,以防操作使用行锁.

  2. 应该使用SET ROWCOUNT来限制将要修改的行数.这里有两个问题:

    1. 自SQL Server 2005发布(11年前)以来,它已被弃用:

      在将来的SQL Server发行版中,使用SET ROWCOUNT不会影响DELETE,INSERT和UPDATE语句.避免在新的开发工作中使用SET ROWCOUNT和DELETE,INSERT和UPDATE语句,并计划修改当前使用它的应用程序.对于类似的行为,请使用TOP语法

    2. 它不仅会影响您正在处理的语句:

      设置SET ROWCOUNT选项会导致大多数Transact-SQL语句在受到指定行数影响时停止处理.这包括触发器.ROWCOUNT选项不会影响动态游标,但它确实限制了键集和不敏感游标的行集.应谨慎使用此选项.

    相反,使用TOP()子句.

  3. 这里没有明确的交易目的.它使代码复杂化并且您没有处理ROLLBACK,因为每个语句都是它自己的事务(即自动提交),所以甚至不需要它.

  4. 假设您找到了保持显式事务的原因,那么您没有TRY/CATCH结构.请在DBA.StackExchange上查看我对处理事务的TRY/CATCH模板的答案:

    我们是否需要在C#代码和商店程序中处理交易

我怀疑真正的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;
Run Code Online (Sandbox Code Playgroud)

通过测试@Rows反对@BatchSize,那么就可以避免最后的更新查询(在大多数情况下),因为最后一组是典型的行少于一定数量的@BatchSize,在这种情况下,我们知道,有没有更多的进程(这是你在输出看到在你的答案中显示).只有在最终行集等于的情况下,@BatchSize此代码才会运行影响0行的最终UPDATE.

我还在WHERE子句中添加了一个条件,以防止已经更新的行再次更新.


mik*_*igs 24

我昨天遇到了这个线程,并根据接受的答案编写了一个脚本。结果发现执行速度非常慢,需要 12 个小时才能处理 33M 行中的 25M。今天早上我最终取消了它,并与 DBA 一起改进它。

DBA 指出is null我的 UPDATE 查询中的检查在 PK 上使用了聚集索引扫描,正是该扫描减慢了查询速度。基本上,查询运行的时间越长,就越需要在索引中查找正确的行。

事后看来,他想出的方法是显而易见的。本质上,您将要更新的行的 ID 加载到临时表中,然后在更新语句中将其连接到目标表中。这使用索引查找而不是扫描。嘿嘿,这确实加快了速度!更新最后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;
Run Code Online (Sandbox Code Playgroud)

批处理速度慢的方式,不要使用!

作为参考,这里是原始的执行速度较慢的查询:

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

Run Code Online (Sandbox Code Playgroud)

  • +1 我同意这是一种非常有效的方法,人们应该尝试一下。如果直接读者遇到像您使用我的方法所遇到的性能问题,我在这里更新了我的答案。我以前使用过这种模式,所以也许我专注于解决OP方法中的缺陷。我确实对你的方法提出了一些小建议,我在答案的末尾详细介绍了这些建议。我没有提到的一件事是简化输出,您可以通过以下方式完成:`DECLARE @Completed INT = 5, @Total INT = 37; RAISERROR('已完成 %d / %d', 10, 1, @Completed, @Total) 现在没有;` (2认同)

Kra*_*amb 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
Run Code Online (Sandbox Code Playgroud)

  • 这是低效的,因为存在检查是多余的。相反,您可以在运行 UPDATE 后检索 @@ROWCOUNT,如果 @@ROWCOUNT &lt;&gt; BatchSize 则您已完成并可以退出循环。 (2认同)
  • @Kramb我知道-您可以根据答案缓存行数/sf/answers/3853800541/ (2认同)

Yar*_*ara 6

我想分享我的经验。几天前,我必须用 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
Run Code Online (Sandbox Code Playgroud)

任务:将人员更新为新的职位名称:“软件开发人员”->“Web 开发人员”。

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
Run Code Online (Sandbox Code Playgroud)

根据行数,此语句将需要一些时间来填充临时表,但它可以避免锁定。在我的情况下,大约需要 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
Run Code Online (Sandbox Code Playgroud)

执行此脚本后,您将收到数百个批次,您可以在 MS SQL Management Studio 的一个选项卡中执行这些批次。

4.运行打印的sql 语句并检查表上的锁。您始终可以停止进程并使用@pageSize来加快或减慢更新速度(不要忘记在暂停脚本后更改@i )。

5.将 Persons_SoftwareDeveloper_To_AspNetDeveloper 删除。删除临时表。

小注意事项:此迁移可能需要一些时间,并且在迁移过程中可能会插入包含无效数据的新行。因此,首先修复行添加的位置。在我的情况下,我修复了 UI,“软件开发人员”->“Web 开发人员”。有关此方法的更多信息,请访问我的博客https://yarkul.com/how-smoothly-insert-millions-of-rows-in-sql-server/


Shi*_*hiv 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
Run Code Online (Sandbox Code Playgroud)

  • @Kramb,如果更新查询中的前 1000 行已设置值,但前 1000 行之外的行没有设置值,则您的解决方案将为无限循环。您的解决方案存在缺陷,因为您缺少对实际 UPDATE 调用的 where 检查。存在检查不是问题。 (2认同)