SQL Server:以小块的形式更新巨大表上的字段:如何获取进度/状态?


10

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

对于日志传送等,显然,我们也希望将其保持在很小的规模上。

  • 下面会解决问题吗?
  • 以及如何获取它以打印一些输出,以便我们可以看到进度?(我们尝试在其中添加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

Answers:


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并且有两个要更新的表(即主表和状态表),我们应该使用显式事务来使这两个表保持同步,但是如果您在某个时间取消了该进程,我们不希望冒孤立事务的风险。开始事务但尚未提交之后的时间点。只要您不停止SQL Agent作业,这样做就应该是安全的。

您如何在不停止的情况下停止该进程?通过要求它停止:-)。是的 通过向进程发送一个“信号”(类似于kill -3Unix),您可以请求该进程在下一个方便的时刻停止(即,当没有活动事务时!),并让其自行清理所有干净整洁的东西。

如何在另一个会话中与正在运行的进程进行通信?通过使用与我们创建的机制相同的机制将其当前状态传达给您:状态表。我们只需要添加一列,该过程将在每个循环的开始进行检查,以便知道是继续还是中止。并且由于目的是将其安排为SQL Agent作业(每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; 

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

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;

是否要暂停该过程,无论它是在SQL Agent作业中运行还是在其他人的计算机上的SSMS中运行?赶紧跑:

UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;

希望该过程能够重新开始备份吗?赶紧跑:

UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;

更新:

这里有一些其他尝试可以改善此操作的性能。没有人可以保证提供帮助,但可能值得测试。随着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. 您可能必须手动更新STATS和/或重建索引以使其保持最新,因为基础数据的更改非常频繁
    4. 请务必记住,索引在帮助的同时SELECT会伤害,UPDATE因为索引是该操作期间必须更新的另一个对象,因此会有更多的I / O。这既可以使用“过滤索引”(由于匹配过滤器的行越少,更新的索引就越小),又需要等待一会儿才能添加索引(如果一开始它并没有多大帮助,那么就没有理由额外的I / O)。

1
太好了 我现在正在运行它,抽烟我们可以在白天运行它。谢谢!
Jonesome恢复Monica 2015年

@samsmith请参阅我刚刚添加的UPDATE部分,因为有一些想法可以使过程更快地执行。
所罗门·鲁兹基

如果没有UPDATE增强功能,我们每小时将获得约800万个更新... @BatchRows设置为10000000(一千万)
Jonesome Reinstate Monica

@samsmith太好了:)对吗?请记住两点:1)由于匹配WHERE子句的行越来越少,因此该过程变慢,因此为什么现在是添加过滤索引的好时机,但是您已经在该位置添加了非过滤索引首先,所以我不确定这是否会有所帮助,但我仍然希望吞吐量随着接近完成而下降,并且2)可以通过将s缩短WAITFOR DELAY至半秒左右提高吞吐量,但这是在并发性和可能通过日志传送发送的量之间进行权衡的。
所罗门·鲁兹基

我们对每小时800万行的速度感到满意。是的,我们可以看到它正在放缓。我们不愿意再创建任何索引(因为该表在整个构建过程中都是锁定的)。我们已经做过几次,就是对现有索引进行重组(因为这是在线的)。
Jonesome恢复莫妮卡的时间

4

回答第二部分:如何在循环期间打印一些输出。

我很少运行sys admin有时必须运行的维护程序。

我从SSMS运行它们,还注意到该PRINT语句仅在整个过程完成后才显示在SSMS中。

因此,我使用的RAISERROR严重性较低:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

我正在使用SQL Server 2008 Standard和SSMS 2012(11.0.3128.0)。这是在SSMS中运行的完整工作示例:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

当我注释掉RAISERROR并仅留下PRINTSSMS的“消息”选项卡中的消息时,仅在整个批处理完成后6秒钟后才出现。

当我注释掉PRINT并使用时RAISERROR,SSMS的“消息”选项卡中的消息无需等待6秒钟即可出现,但随着循环的进行而增加。

有趣的是,当我同时使用RAISERROR和时PRINT,都会看到两条消息。首先是来自first的消息RAISERROR,然后延迟2秒钟,然后是first PRINT和second RAISERROR,依此类推。


在其他情况下,我使用单独的专用log表,并在表中简单插入一行,其中包含一些信息,这些信息描述了长时间运行的进程的当前状态和时间戳。

在长时间运行的过程中,我定期SELECTlog表中查看发生了什么。

这显然有一定的开销,但是留下了日志(或日志历史记录),我以后可以按照自己的速度进行检查。


在SQL 2008/2014上,我们看不到raiseerror的结果...。我们缺少什么?
Jonesome恢复Monica 2015年

@samsmith,我添加了一个完整的示例。试试看。在这个简单的示例中,您得到什么行为?
弗拉基米尔·巴拉诺夫

2

您可以使用以下类似方法从另一个连接监视它:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

看还有多少事要做。如果应用程序正在调用进程,而不是您在SSMS或类似程序中手动运行它,并且需要显示进度,则这可能会很有用:异步运行主进程(或在另一个线程上),然后循环调用“剩余多少”。请每隔一段时间检查一次,直到异步调用(或线程)完成为止。

将隔离级别设置得尽可能宽松意味着这应该在合理的时间内返回,而不会由于锁定问题而滞留在主事务之后。这可能意味着返回的值当然有点不准确,但是作为一个简单的进度表,这根本不重要。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.