sql server:以小块更新大表上的字段:如何获取进度/状态?

Jon*_*ica 10 sql-server t-sql

我们有一个非常大(1 亿行)的表,我们需要更新其中的几个字段。

对于日志运输等,我们显然也希望将其保持为一口大小的交易。

  • 下面会做的伎俩吗?
  • 我们如何让它打印一些输出,以便我们可以看到进度?(我们尝试在其中添加 PRINT 语句,但在 while 循环期间没有输出任何内容)

代码是:

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END
Run Code Online (Sandbox Code Playgroud)

Sol*_*zky 12

我在回答相关问题时没有意识到这个问题(在这个 while 循环中是否需要显式交易?),但为了完整起见,我将在这里解决这个问题,因为它不是我在链接答案中的建议的一部分.

由于我建议通过 SQL Agent 作业(毕竟它是 1 亿行)来安排它,因此我认为向客户端(即 SSMS)发送状态消息的任何形式都不是理想的(尽管如果是如果需要其他项目,那么我同意 Vladimir 的观点,即使用RAISERROR('', 10, 1) WITH NOWAIT;是可行的方法)。

在这种特殊情况下,我将创建一个状态表,可以在每个循环中使用迄今为止更新的行数进行更新。投入当前时间来关注这个过程并没有什么坏处。

鉴于您希望能够取消并重新启动该过程, 我厌倦了将主表的 UPDATE 与状态表的 UPDATE 包装在显式事务中。但是,如果您觉得由于取消而导致状态表不同步,只需使用COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL.并且有两个表要 UPDATE(即主表和状态表),我们应该使用显式事务来保持这两个表同步,但是如果您在某个时间取消进程,我们不想冒孤立事务的风险在它开始事务但尚未提交之后的点。只要您不停止 SQL 代理作业,这应该是安全的。

你怎么能在没有,嗯,好吧,停止它的情况下停止这个过程?通过要求它停止:-)。是的。通过向进程发送一个“信号”(类似于kill -3在 Unix 中),您可以请求它在下一个方便的时刻停止(即当没有活动事务时!)并让它自己清理干净整洁。

如何在另一个会话中与正在运行的进程通信?通过使用我们为其创建的相同机制将其当前状态传达给您:状态表。我们只需要添加一个列,进程将在每个循环开始时检查该列,以便它知道是继续还是中止。由于目的是将其安排为 SQL 代理作业(每 10 或 20 分钟运行一次),我们还应该在开始时进行检查,因为如果进程刚刚开始,则用 100 万行填充临时表是没有意义的稍后退出,不使用任何数据。

DECLARE @BatchRows INT = 1000000,
        @UpdateRows INT = 4995;

IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
  CREATE TABLE dbo.HugeTable_TempStatus
  (
    RowsUpdated INT NOT NULL, -- updated by the process
    LastUpdatedOn DATETIME NOT NULL, -- updated by the process
    PauseProcess BIT NOT NULL -- read by the process
  );

  INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
  VALUES (0, GETDATE(), 0);
END;

-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
  PRINT 'Process is paused. No need to start.';
  RETURN;
END;

CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);

INSERT INTO #FullSet (KeyField1, KeyField2)
  SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
  FROM   dbo.HugeTable ht
  WHERE  ht.deleted IS NULL
  OR     ht.deletedDate IS NULL

WHILE (1 = 1)
BEGIN
  -- Check if process is paused. If yes, just exit cleanly.
  IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
  BEGIN
    PRINT 'Process is paused. Exiting.';
    BREAK;
  END;

  -- grab a set of rows to update
  DELETE TOP (@UpdateRows)
  FROM   #FullSet
  OUTPUT Deleted.KeyField1, Deleted.KeyField2
  INTO   #CurrentSet (KeyField1, KeyField2);

  IF (@@ROWCOUNT = 0)
  BEGIN
    RAISERROR(N'All rows have been updated!!', 16, 1);
    BREAK;
  END;

  BEGIN TRY
    BEGIN TRAN;

    -- do the update of the main table
    UPDATE ht
    SET    ht.deleted = 0,
           ht.deletedDate = '2000-01-01'
    FROM   dbo.HugeTable ht
    INNER JOIN #CurrentSet cs
            ON cs.KeyField1 = ht.KeyField1
           AND cs.KeyField2 = ht.KeyField2;

    -- update the current status
    UPDATE ts
    SET    ts.RowsUpdated += @@ROWCOUNT,
           ts.LastUpdatedOn = GETDATE()
    FROM   dbo.HugeTable_TempStatus ts;

    COMMIT TRAN;
  END TRY
  BEGIN CATCH
    IF (@@TRANCOUNT > 0)
    BEGIN
      ROLLBACK TRAN;
    END;

    THROW; -- raise the error and terminate the process
  END CATCH;

  -- clear out rows to update for next iteration
  TRUNCATE TABLE #CurrentSet;

  WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;

-- clean up temp tables when testing
-- DROP TABLE #FullSet; 
-- DROP TABLE #CurrentSet; 
Run Code Online (Sandbox Code Playgroud)

然后,您可以随时使用以下查询检查状态:

SELECT sp.[rows] AS [TotalRowsInTable],
       ts.RowsUpdated,
       (sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
       ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE  sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND    sp.[index_id] < 2;
Run Code Online (Sandbox Code Playgroud)

想要暂停进程,无论它是在 SQL 代理作业中运行,还是在其他人计算机上的 SSMS 中运行?赶紧跑:

UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;
Run Code Online (Sandbox Code Playgroud)

希望该过程能够再次开始备份?赶紧跑:

UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;
Run Code Online (Sandbox Code Playgroud)

更新:

这里有一些额外的尝试,可能会提高此操作的性能。没有一个保证有帮助,但可能值得测试。并且有 1 亿行要更新,您有足够的时间/机会来测试一些变化;-)。

  1. 添加TOP (@UpdateRows)到 UPDATE 查询,使第一行看起来像:
    UPDATE TOP (@UpdateRows) ht
    有时它可以帮助优化器知道有多少行会受到影响,这样它就不会浪费时间寻找更多行。
  2. 将 PRIMARY KEY 添加到#CurrentSet临时表。这里的想法是帮助优化器将 JOIN 连接到 1 亿行表。

    并且只是为了避免歧义,没有任何理由将 PK 添加到#FullSet临时表,因为它只是一个简单的队列表,其中的顺序无关紧要。

  3. 在某些情况下,它有助于添加一个过滤索引来帮助SELECT输入#FullSet临时表的索引。以下是与添加此类索引相关的一些注意事项:
    1. WHERE 条件应与查询的 WHERE 条件匹配,因此 WHERE deleted is null or deletedDate is null
    2. 在过程开始时,大多数行将匹配您的 WHERE 条件,因此索引没有那么有用。您可能需要等到 50% 左右的某个地方再添加它。当然,它有多大帮助以及何时最好添加索引因多种因素而异,因此有点反复试验。
    3. 您可能需要手动 UPDATE STATS 和/或 REBUILD 索引以使其保持最新状态,因为基础数据变化非常频繁
    4. 一定要记住,索引在帮助 的同时SELECT会伤害 ,UPDATE因为它是另一个必须在该操作期间更新的对象,因此需要更多的 I/O。这既可以使用过滤索引(由于与过滤器匹配的行较少,因此在更新行时会缩小),并等待一段时间添加索引(如果它在开始时不会非常有用,那么没有理由招致额外的 I/O)。