【问题标题】:How to avoid UPDATE statement locking out the entire table when updating large number of records更新大量记录时如何避免UPDATE语句锁定整个表
【发布时间】:2017-01-24 18:18:19
【问题描述】:

我对锁和提示相当陌生。

我有一个非常频繁的SELECTINSERT 操作的表。该表有 1100 万条记录。

我已向其中添加了一个新列,我需要将同一表中现有列中的数据复制到新列中。

我打算使用ROWLOCK 提示来避免将锁升级到表级锁并阻止表上的所有其他操作。例如:

UPDATE 
    SomeTable WITH (ROWLOCK)
SET
    NewColumn = OldColumn

问题:

  1. 要用NOLOCK 代替ROWLOCK 吗?注意,一旦记录插入到表中,OldColumn 的值就不会改变,所以NOLOCK 不会造成脏读。
  2. NOLOCK 在这种情况下是否有意义,因为 SQL Server 无论如何都必须为 UPDATE 获取更新锁。
  3. 有没有更好的方法来实现这一点?

我知道要避免提示,SQL Server 通常会做出更明智的选择,但我不想在此更新期间锁定表。

【问题讨论】:

  • 您拥有的语句将在单个事务中更新表中的所有记录,因此无论您如何剪切它,您都将锁定事务中的所有记录。
  • 我曾经看到 Kendra Little 在更新方面做得很好。更新不会并行,但为了加快速度,她在 cte 中使用 select 来实现并行读取。

标签: sql-server tsql


【解决方案1】:

尝试批量更新。

DECLARE @Batch INT = 1000
DECLARE @Rowcount INT = @Batch


WHILE @Rowcount > 0
    BEGIN
        ;WITH CTE AS 
        (
            SELECT TOP (@Batch) NewColumn,OldColumn 
            FROM SomeTable 
            WHERE NewColumn <> OldColumn
                  OR (NewColumn IS NULL AND OldColumn IS NOT NULL)
        )
        UPDATE cte
            SET NewColumn = OldColumn;
        SET @Rowcount = @@ROWCOUNT
    END

【讨论】:

  • 感谢您展示 CTE 更新级联到基础表。
  • 在我的回答中使用update...top 组合的批量更新的另一种变体。
  • 这不只是更新 CTE 而不是实际的 SomeTable 吗?
  • @Basil CTE 不是对象,它是对象 SomeTable 的表示。如果您查看使用 CTE 的查询的执行计划,就优化器而言,CTE 不存在。
【解决方案2】:

我采用了@pacreely 的批量更新方法(参见他对这个问题的回答)并创建了一个update...top 变体。我添加了 (rowlock) 提示,告诉 SQL Server 将锁保持在行级别。

详情请参阅update...top。另请注意,在updateinsertmergedelete 语句中使用top 时不能使用order by,因此引用的行不按任何顺序排列。

declare @BatchSize  int = 1000
declare @RowCount   int = @BatchSize

while @RowCount > 0
begin
    update top (@BatchSize) SomeTable with (rowlock)
    set NewColumn = OldColumn
    where 
        NewColumn <> OldColumn      or
        (
            NewColumn is null       and
            OldColumn is not null
        )
    select @RowCount = @@rowcount
end

【讨论】:

    【解决方案3】:

    【讨论】:

      【解决方案4】:

      我们最近遇到了一个案例,我们想做类似的事情,但要在几天内缓慢进行(每次运行只更新一定数量的记录,并且只在特定时间进行)。最新数据很好,但需要更新数百万行旧数据。我们的数据表如下所示:

      Create Table FileContent
      (
      FileContent varchar(max),
      File_PK bigint,
      NewFileContent varchar(max)
      )
      

      我们只需要更新某些行,但需要更新数百万行。我们创建了一个表来存储我们的进度,这样我们就可以使用计划的作业来迭代和更新主表,然后用需要更新的主表记录的主键填充这个表:

      Create Table FilesToUpdate
      (
      File_PK bigint,
      IsUpdated bit NOT NULL DEFAULT 0
      )
      

      然后我们安排了以下脚本来进行更新(供您自己使用,请根据您的系统使用批量大小和安排)。

      /***  
      Script to update and fix records.
      ***/
      DECLARE @Rowcount INT = 1 -- 
          ,   @BatchSize INT = 100 -- how many rows will be updated on each iteration of the loop 
          ,   @BatchesToRun INT = 25 -- the max number of times the loop will iterate
          ,   @StartingRecord BIGINT = 1;
      
      -- Get the highest File_PK not already fixed as a starting point.
      Select @StartingRecord = MAX(File_PK) From FilesToUpdate where IsUpdated = 0
      
      -- While there are still rows to update and we haven't hit our limit on iterations... 
      WHILE (@Rowcount > 0 and @BatchesToRun > 0)   
      BEGIN
          print Concat('StartingRecord (Start of Loop): ', @StartingRecord)
          UPDATE FileContent SET  NewFileContent = 'New value here'
          WHERE File_PK BETWEEN (@StartingRecord - @BatchSize + 1) AND @StartingRecord;
      
          -- @@Rowcount is the number of records affected by the last statement.  If this returns 0, the loop will stop because we've run out of things to update.
          SET @Rowcount = @@ROWCOUNT;
          print Concat('RowCount: ', @Rowcount)
      
          -- Record which PKs were updated so we know where to start next time around.
          UPDATE FilesToUpdate Set IsUpdated = 1 where File_PK BETWEEN (@StartingRecord - @BatchSize + 1) AND @StartingRecord;
      
          -- The loop will stop after @BatchSize*@BatchesToRun records are updated. 
          -- If there aren't that many records left to update, the @Rowcount checks will stop it. 
          SELECT @BatchesToRun = @BatchesToRun - 1
          print Concat('Batches Remaining: ',@BatchesToRun)
      
          -- Set the starting record for the next time through the loop.
          SELECT @StartingRecord -= @BatchSize
          print Concat('StartingRecord (End of Loop): ', @StartingRecord)
      END
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-10-18
        • 2021-08-03
        • 2013-10-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多