从SQL表中删除数百万行


9

我必须从221+百万行表中删除16+百万条记录,并且运行非常缓慢。

多谢您分享建议,以加快以下代码的速度:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

DECLARE @BATCHSIZE INT,
        @ITERATION INT,
        @TOTALROWS INT,
        @MSG VARCHAR(500);
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;

BEGIN TRY
    BEGIN TRANSACTION;

    WHILE @BATCHSIZE > 0
        BEGIN
            DELETE TOP (@BATCHSIZE) FROM MySourceTable
            OUTPUT DELETED.*
            INTO MyBackupTable
            WHERE NOT EXISTS (
                                 SELECT NULL AS Empty
                                 FROM   dbo.vendor AS v
                                 WHERE  VendorId = v.Id
                             );

            SET @BATCHSIZE = @@ROWCOUNT;
            SET @ITERATION = @ITERATION + 1;
            SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
            SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);             
            PRINT @MSG;
            COMMIT TRANSACTION;
            CHECKPOINT;
        END;
END TRY
BEGIN CATCH
    IF @@ERROR <> 0
       AND @@TRANCOUNT > 0
        BEGIN
            PRINT 'There is an error occured.  The database update failed.';
            ROLLBACK TRANSACTION;
        END;
END CATCH;
GO

执行计划(限于2次迭代)

在此处输入图片说明

VendorIdPK并且是非集群的,此脚本未使用聚集索引。还有5个其他非唯一,非聚集索引。

任务是“删除另一个表中不存在的供应商”,并将它们备份到另一个表中。我有3张桌子vendors, SpecialVendors, SpecialVendorBackups。尝试删除表中SpecialVendors不存在的Vendors记录,并备份已删除的记录,以防万一我做错了,我必须将它们放回一两周。


我会优化该查询,然后尝试在null为空的情况下进行左
狗仔队

Answers:


8

执行计划表明,它正在以某种顺序从非聚集索引读取行,然后对读取的每个外部行执行查找以评估 NOT EXISTS

在此处输入图片说明

您将删除表的7.2%。3,556批次4,500中的16,000,000行

假设合格的行最终分布在整个索引中,则意味着它将每13.8行删除大约1行。

因此,迭代1将读取62,156行,并执行许多索引查找,然后才发现要删除4,500行。

迭代2将读取57,656(62,156-4,500)行,这些​​行绝对不能忽略任何并发更新(因为它们已被处理),然后再读取62,156行以删除4,500。

迭代3将读取(2 * 57,656)+ 62,156行,依此类推,直到最终迭代3,556将读取(3555 * 57,656)+ 62,156行,并执行许多次搜索。

因此,所有批次中执行的索引查找的数量为 SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

哪个是((3555 * 3556 / 2) * 57656) + (3556 * 62156)-或364,652,494,976

我建议您先具体化要删除的行到临时表中

INSERT INTO #MyTempTable
SELECT MySourceTable.PK,
       1 + ( ROW_NUMBER() OVER (ORDER BY MySourceTable.PK) / 4500 ) AS BatchNumber
FROM   MySourceTable
WHERE  NOT EXISTS (SELECT *
                   FROM   dbo.vendor AS v
                   WHERE  VendorId = v.Id) 

并更改DELETE为delete 由于临时表已被填充,因此WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)您可能仍需要NOT EXISTSDELETE查询本身中包含a 来满足更新需求,但这应该更加有效,因为每批只需要执行4,500次搜索。


当您说“首先将要删除的行具体化到临时表中”时,是否建议将所有这些记录及其所有列都放入临时表中?还是只有PK列? (我相信您建议我将它们完全移到临时表,但要仔细检查)
cilerler

@cilerler-关键要点
马丁·史密斯

你能快速查看这个,如果我让你正确或不说什么,好吗?
cilerler '17

@cilerler- DELETE TOP (@BATCHSIZE) FROM MySourceTable应该只是DELETE FROM MySourceTable 临时表的索引,CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );并且VendorId绝对是PK本身吗?您有超过2.21亿个不同的供应商?
马丁·史密斯,

感谢Martin,请在下午6点之后进行测试。您的回答是,这绝对是该表中存在的唯一PK
cilerler

4

执行计划表明,每个后续循环将比前一个循环做更多的工作。假设要删除的行在整个表中均匀分布,则第一个循环将需要扫描约4500 * 221000000/16000000 = 62156行,以查找要删除的4500行。它还将对表执行相同数量的聚簇索引查找vendor。但是,第二个循环将需要读取您第一次没有删除的相同的62156-4500 = 57656行。我们可能希望第二个循环从MySourceTablevendor表扫描120000行并对该表执行120000个查找。每个循环所需的工作量以线性速率增加。可以近似地说,平均循环将需要从中读取102516868行MySourceTable并执行102516868针对vendor表。要删除批量大小为4500的1600万行,您的代码需要执行16000000/4500 = 3556循环,因此,要完成代码的总工作量约为读取3645亿行MySourceTable和3645亿索引查找。

一个较小的问题是,您@BATCHSIZE在TOP表达式中使用局部变量而没有RECOMPILE或其他提示。创建计划时,查询优化器将不知道该局部变量的值。它将假定它等于100。实际上,您删除的是4500行而不是100行,由于这种差异,您最终可能会获得效率较低的计划。将低基数估计值插入表中时也会导致性能下降。如果SQL Server认为需要插入100行而不是4500行,则可以选择其他内部API进行插入。

一种替代方法是简单地将要删除的行的主键/集群键插入临时表。根据您的键列的大小,这很容易适合tempdb。在这种情况下,您可以获得最少的日志记录,这意味着事务日志不会中断。对于恢复模式为的任何数据库,您还可以获得最少的日志记录SIMPLE。有关要求的更多信息,请参见链接。

如果不是这样,则应该更改代码,以便可以利用上的聚簇索引MySourceTable。关键是编写代码,以便每个循环执行大约相同的工作量。您可以利用索引来做到这一点,而不必每次都从头开始扫描表。我写了一篇博客文章,介绍了一些不同的循环方法。该文章中的示例确实将插入表而不是删除表,但是您应该能够修改代码。

在下面的示例代码中,我假定您的主键和集群键MySourceTable。我很快编写了这段代码,但无法对其进行测试:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

DECLARE @BATCHSIZE INT,
        @ITERATION INT,
        @TOTALROWS INT,
        @MSG VARCHAR(500)
        @STARTID BIGINT,
        @NEXTID BIGINT;
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;

SELECT @STARTID = ID
FROM MySourceTable
ORDER BY ID
OFFSET 0 ROWS
FETCH FIRST 1 ROW ONLY;

SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;

BEGIN TRY
    BEGIN TRANSACTION;

    WHILE @STARTID IS NOT NULL
        BEGIN
            WITH MySourceTable_DELCTE AS (
                SELECT TOP (60000) *
                FROM MySourceTable
                WHERE ID >= @STARTID
                ORDER BY ID
            )           
            DELETE FROM MySourceTable_DELCTE
            OUTPUT DELETED.*
            INTO MyBackupTable
            WHERE NOT EXISTS (
                                 SELECT NULL AS Empty
                                 FROM   dbo.vendor AS v
                                 WHERE  VendorId = v.Id
                             );

            SET @BATCHSIZE = @@ROWCOUNT;
            SET @ITERATION = @ITERATION + 1;
            SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
            SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);             
            PRINT @MSG;
            COMMIT TRANSACTION;

            CHECKPOINT;

            SET @STARTID = @NEXTID;
            SET @NEXTID = NULL;

            SELECT @NEXTID = ID
            FROM MySourceTable
            WHERE ID >= @STARTID
            ORDER BY ID
            OFFSET (60000) ROWS
            FETCH FIRST 1 ROW ONLY;

        END;
END TRY
BEGIN CATCH
    IF @@ERROR <> 0
       AND @@TRANCOUNT > 0
        BEGIN
            PRINT 'There is an error occured.  The database update failed.';
            ROLLBACK TRANSACTION;
        END;
END CATCH;
GO

关键部分在这里:

WITH MySourceTable_DELCTE AS (
    SELECT TOP (60000) *
    FROM MySourceTable
    WHERE ID >= @STARTID
    ORDER BY ID
)   

每个循环只会从中读取60000行MySourceTable。这将导致每个事务平均删除大小为4500行,每个事务最大删除大小为60000行。如果您希望使用较小的批处理量更加保守,那也可以。在@STARTID每一个循环之后变量的进步,所以你可以避免读取同一行超过从源表一次。


感谢您提供详细信息。我将那4500个限制设置为不锁定表。如果我没有记错的话,SQL的硬性限制是:如果删除计数超过5000,则锁定整个表。由于这将是一个漫长的过程,因此我不愿意长时间锁定该表。如果我将60000设置为4500,那么您认为我会获得相同的效果吗?
cilerler '17

@cilerler如果您担心锁升级,可以在表级别将其禁用。使用4500的批处理大小没有问题。关键是每个循环将执行大致相同的工作量。
Joe Obbish

由于速度差异,我不得不接受其他答案。我测试了您的解决方案和@ Martin-Smith的解决方案,他的版本在10分钟的测试中获得了约2%的更多数据。您的解决方案比我的解决方案好得多,我真的很感谢您的
宝贵

2

我想到了两个想法:

延迟可能是由于使用该数据量编制索引。尝试删除索引,删除并重新构建索引。

要么..

将要保留的行复制到临时表中,删除具有1600万行的表,然后重命名临时表(或复制到源表的新实例)可能会更快。

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.