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 회 반복으로 제한)

여기에 이미지 설명을 입력하십시오

VendorId이다 PK클러스터되지 않은 경우, 클러스터 된 인덱스는 이 스크립트에 의해 사용되지 않습니다. 5 개의 다른 고유하지 않은 클러스터되지 않은 인덱스가 있습니다.

작업은 "다른 테이블에 존재하지 않는 공급 업체를 제거"하고 다른 테이블에 백업하는 것입니다. 3 개의 테이블이 vendors, SpecialVendors, SpecialVendorBackups있습니다. 테이블에 SpecialVendors존재하지 않는 것을 제거 Vendors하고 내가하고있는 일이 잘못되어 삭제 된 레코드를 백업하려고하면 1 ~ 2 주 안에 다시 넣어야합니다.


나는 그 쿼리를 최적화하고 왼쪽 조인을 시도 할 것이다
paparazzo

답변:


8

실행 계획은 클러스터되지 않은 인덱스에서 어떤 순서로 행을 읽은 다음 각 외부 행 읽기에 대한 탐색을 수행하여 NOT EXISTS

여기에 이미지 설명을 입력하십시오

테이블의 7.2 %를 삭제하고 있습니다. 4,500 개의 3,556 개 배치에서 16,000,000 개의 행

규정 된 행이 결국 인덱스 전체에 분배된다고 가정하면 13.8 행마다 약 1 개의 행이 삭제됩니다.

따라서 반복 1은 62,156 행을 읽고 삭제할 인덱스가 4,500 개가되기 전에 많은 인덱스 탐색을 수행합니다.

반복 2는 57,656 (62,156-4,500) 개의 행을 읽으며 (이미 처리 된 것과 같이) 동시 업데이트를 무시하고 4,500을 삭제하기 위해 다른 62,156 행을 무시할 수는 없습니다.

반복 3은 (2 * 57,656) + 62,156 행을 읽습니다. 마지막으로 반복 3,556은 (3,555 * 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삭제 WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)당신은 여전히 포함해야 NOT EXISTS에서 DELETE임시 테이블을 채워 이후 업데이트를 수용 쿼리 자체 만 만 4,500 일괄 당 추구 수행해야하므로이 훨씬 더 효율적이어야한다.


"임시 테이블에 삭제할 행을 먼저 구체화"라고 말하면 모든 열이있는 모든 레코드를 임시 테이블에 배치하는 것이 좋습니다. 아니면 PK열만? (나는 당신이 그것들을 임시 테이블로 옮기라고 제안하지만 더블 체크하고 싶었다)
cilerler

@cilerler-바로 핵심 칼럼
Martin Smith

당신은 신속하게 검토 할 수 있습니다 난 당신이 제대로 여부를하시기 바랍니다 말을 얻는 경우에?
cilerler

@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTable바로해야한다 DELETE FROM MySourceTable 또한 임시 테이블의 인덱스 CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );이며 VendorId확실히 자신의 PK? 2 억 1 천 2 백만 이상의 공급 업체가 있습니까?
Martin Smith

고마워 마틴 오후 6시 이후에 테스트합니다. 그리고 당신의 대답은, 그것은 확실히 그 테이블에 존재하는 유일한 PK입니다
cilerler

4

실행 계획은 각 연속 루프가 이전 루프보다 더 많은 작업을 수행 할 것을 제안합니다. 삭제할 행이 테이블 전체에 균등하게 분산되어 있다고 가정하면 첫 번째 루프는 약 4500 * 221000000 / 16000000 = 62156 행을 스캔하여 삭제할 4500 행을 찾습니다. 또한 vendor테이블 에 대해 동일한 수의 클러스터 된 인덱스 탐색을 수행 합니다. 그러나 두 번째 루프는 처음 삭제하지 않은 동일한 62156-4500 = 57656 행을 지나야합니다. 두 번째 루프가 테이블 에서 120000 개의 행을 스캔하고 120000 개의 MySourceTable탐색을 수행 할 것으로 예상 할 수 있습니다 vendor. 루프 당 필요한 작업량은 선형 속도로 증가합니다. 근사치로 우리는 평균 루프가 102516868 행을 읽어야 MySourceTable하고 102516868 탐색은vendor표. 배치 크기 4500의 1 천 6 백만 행을 삭제하려면 코드에서 16000000/4500 = 3556 루프를 수행해야하므로 코드를 완료하는 데 필요한 총 작업량은 약 3,460 억 행을 읽고 MySourceTable3,460 억 인덱스를 찾는 것입니다.

더 작은 문제는 @BATCHSIZETOP 식에서 로컬 변수를 사용 RECOMPILE하거나 다른 힌트 를 사용하지 않는다는 것 입니다. 쿼리 최적화 프로그램은 계획을 만들 때 해당 로컬 변수의 값을 알지 못합니다. 실제로는 100과 같다고 가정합니다. 실제로 100 개가 아닌 4500 개의 행을 삭제하면 해당 불일치로 인해 계획이 덜 효율적일 수 있습니다. 테이블에 삽입 할 때 카디널리티 추정치가 낮 으면 성능 저하가 발생할 수 있습니다. SQL Server는 4500 개의 행이 아니라 100 개의 행을 삽입해야한다고 생각되면 삽입을 수행하기 위해 다른 내부 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 제한을 설정했습니다. 내가 실수하지 않으면 삭제 횟수가 5000을 초과하면 SQL에 전체 테이블을 잠그는 하드 한계가 있습니다. 프로세스가 길기 때문에 오랫동안 테이블을 잠그려고 노력할 수 없습니다. 60000을 4500으로 설정하면 동일한 성능을 얻을 것이라고 생각합니까?
cilerler

@cilerler 잠금 에스컬레이션이 걱정되면 테이블 레벨에서 사용하지 않도록 설정할 수 있습니다. 배치 크기 4500을 사용하는 데 아무런 문제가 없습니다. 핵심은 각 루프가 대략 같은 양의 작업을 수행한다는 것입니다.
Joe Obbish

속도 차이로 인해 다른 답변을 수락해야합니다. 귀하의 솔루션과 @ Martin-Smith의 솔루션을 테스트했으며 그의 버전은 10 분 테스트 동안 ~ 2 %의 데이터를 더 많이 얻습니다. 당신의 솔루션은 나의 것보다 훨씬 낫고 당신의 시간에 정말 감사합니다 ... –
cilerler

2

두 가지 생각이 떠 오릅니다.

지연은 아마도 해당 데이터 볼륨을 사용하여 인덱싱하기 때문일 수 있습니다. 인덱스를 삭제하고 인덱스를 삭제 한 후 다시 작성하십시오.

또는..

유지하려는 행을 임시 테이블에 복사하고 1,600 만 행으로 테이블을 삭제하고 임시 테이블의 이름을 바꾸거나 소스 테이블의 새 인스턴스에 복사하는 것이 더 빠를 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.