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

时间:2016-03-09 21:52:22

标签: 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

查询 - 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

我在这里缺少什么?

7 个答案:

答案 0 :(得分:24)

  1. 您不应该更新集合中的10k行,除非您确定该操作正在获取Page Locks(由于每页多行是UPDATE操作的一部分)。问题是Lock Escalation(从Row或Page到Table锁定)发生在5000 locks 。所以最安全的是将它保持在5000以下,以防操作使用行锁。

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

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

        

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

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

        

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

    3. 相反,请使用TOP()子句。

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

    5. 假设您找到了保留显式事务的原因,那么您没有TRY / CATCH结构。有关处理事务的TRY / CATCH模板,请参阅我在DBA.StackExchange上的答案:

      Are we required to handle Transaction in C# Code as well as in Store procedure

    6. 我怀疑问题中的示例代码中没有显示真正的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)