Excluir milhões de linhas de uma tabela SQL

9

Eu tenho que excluir mais de 16 milhões de registros de uma tabela de mais de 221 milhões e está indo muito devagar.

Agradeço se você compartilhar sugestões para tornar o código abaixo mais rápido:

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

Plano de execução (limitado a 2 iterações)

insira a descrição da imagem aqui

VendorIdé PK e não clusterizado , em que o índice clusterizado não está sendo usado por esse script. Existem outros 5 índices não exclusivos e sem cluster.

A tarefa é "remover fornecedores que não existem em outra tabela" e fazer backup deles em outra tabela. Eu tenho 3 mesas vendors, SpecialVendors, SpecialVendorBackups. Tentando remover o SpecialVendorsque não existe na Vendorstabela e ter um backup dos registros excluídos, caso o que estou fazendo esteja errado e precise devolvê-los em uma semana ou duas.

cilerler
fonte
Gostaria de trabalhar em otimizar essa consulta e tentar uma associação à esquerda, onde nula
paparazzo

Respostas:

8

O plano de execução mostra que ele está lendo linhas de um índice não clusterizado em alguma ordem e, em seguida, realiza pesquisas para cada linha externa lida para avaliar o NOT EXISTS

insira a descrição da imagem aqui

Você está excluindo 7,2% da tabela. 16.000.000 linhas em 3.556 lotes de 4.500

Supondo que as linhas que se qualificam sejam distribuídas de forma constante por todo o índice, isso significa que ele excluirá aproximadamente 1 linha a cada 13,8 linhas.

Portanto, a iteração 1 lê 62.156 linhas e executa muitas buscas de índice antes de encontrar 4.500 para excluir.

a iteração 2 lerá 57.656 (62.156 - 4.500) linhas que definitivamente não se qualificarão, ignorando atualizações simultâneas (como já foram processadas) e depois outras 62.156 linhas para obter 4.500 para excluir.

a iteração 3 lerá (2 * 57.656) + 62.156 linhas e assim por diante até que finalmente a iteração 3.556 lerá (3.555 * 57.656) + 62.156 linhas e executará muitas buscas.

Portanto, o número de pesquisas de índice realizadas em todos os lotes é SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Qual é ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- ou364,652,494,976

Sugiro que você materialize as linhas para excluir primeiro uma tabela temporária

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) 

E altere DELETEpara excluir WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Você ainda pode precisar incluir um NOT EXISTSna DELETEprópria consulta para atender às atualizações, pois a tabela temporária foi preenchida, mas isso deve ser muito mais eficiente, pois será necessário realizar 4.500 buscas por lote.

Martin Smith
fonte
Quando você diz "materializar as linhas a serem excluídas primeiro em uma tabela temporária", está sugerindo colocar todos esses registros com todas as suas colunas na tabela temporária? ou apenas a PKcoluna? (Eu acredito que você está me sugerindo para mover aqueles a tabela temporária completamente, mas queria verifique)
cilerler
@cilerler - Apenas a (s) coluna (s) chave (s)
Martin Smith
você pode revisar rapidamente isso se eu entendi o que você disse corretamente ou não, por favor?
cilerler
@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTabledeve apenas ser DELETE FROM MySourceTable também indexar a tabela temporária CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );e é VendorIddefinitivamente o PK por conta própria? Você tem> 221 milhões de fornecedores diferentes?
Martin Smith
Obrigado Martin, Will testá-lo depois das 18:00. E sua resposta é, é definitivamente a única exist PK na tabela
cilerler
4

O plano de execução sugere que cada loop sucessivo fará mais trabalho que o loop anterior. Supondo que as linhas a serem excluídas sejam distribuídas igualmente por toda a tabela, o primeiro loop precisará varrer cerca de 4500 * 221000000/16000000 = 62156 linhas para encontrar 4500 linhas a serem excluídas. Ele também fará o mesmo número de pesquisas de índice em cluster na vendortabela. No entanto, o segundo loop precisará ler além das mesmas linhas 62156 - 4500 = 57656 que você não excluiu da primeira vez. Podemos esperar que o segundo loop verifique 120000 linhas de MySourceTablee faça 120000 buscas na vendortabela. A quantidade de trabalho necessária por loop aumenta a uma taxa linear. Como uma aproximação, podemos dizer que o loop médio precisará ler 102516868 linhas de MySourceTablee para fazer 102516868 buscas em relação aovendormesa. Para excluir 16 milhões de linhas com um tamanho de lote de 4500, seu código precisa executar 16000000/4500 = 3556 loops, para que a quantidade total de trabalho para seu código seja concluída em torno de 364,5 bilhões de linhas lidas MySourceTablee 364,5 bilhões de pesquisas de índice.

Um problema menor é que você usa uma variável local @BATCHSIZEem uma expressão TOP sem uma RECOMPILEou outra dica. O otimizador de consulta não saberá o valor dessa variável local ao criar um plano. Ele assumirá que é igual a 100. Na realidade, você está excluindo 4500 linhas em vez de 100 e pode acabar com um plano menos eficiente devido a essa discrepância. A estimativa de baixa cardinalidade ao inserir em uma tabela também pode causar um impacto no desempenho. O SQL Server pode escolher uma API interna diferente para inserir, se achar que precisa inserir 100 linhas, em vez de 4500 linhas.

Uma alternativa é simplesmente inserir as chaves primárias / chaves em cluster das linhas que você deseja excluir em uma tabela temporária. Dependendo do tamanho das suas colunas-chave, isso pode caber facilmente no tempdb. Nesse caso, você pode obter um registro mínimo, o que significa que o log de transações não explodirá. Você também pode obter log mínimo em qualquer banco de dados com um modelo de recuperação SIMPLE. Consulte o link para obter mais informações sobre os requisitos.

Se isso não for uma opção, altere seu código para aproveitar o índice em cluster MySourceTable. O importante é escrever seu código para que você faça aproximadamente a mesma quantidade de trabalho por loop. Você pode fazer isso aproveitando o índice em vez de apenas verificar a tabela desde o início de cada vez. Eu escrevi um post de blog que aborda alguns métodos diferentes de loop. Os exemplos nesse post são inseridos em uma tabela em vez de exclusões, mas você deve conseguir adaptar o código.

No código de exemplo abaixo, assumo que a chave primária e a chave em cluster do seu MySourceTable. Escrevi esse código rapidamente e não consigo testá-lo:

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

A parte principal está aqui:

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

Cada loop lê apenas 60000 linhas de MySourceTable. Isso deve resultar em um tamanho médio de exclusão de 4500 linhas por transação e um tamanho máximo de exclusão de 60000 linhas por transação. Se você quer ser mais conservador com um tamanho de lote menor, isso também é bom. A @STARTIDvariável avança após cada loop, para evitar a leitura da mesma linha mais de uma vez na tabela de origem.

Joe Obbish
fonte
Obrigado por informações detalhadas. Defino esse limite de 4500 para não bloquear a tabela. Se não me engano, o SQL tem um limite rígido que bloqueia a tabela inteira se a contagem de exclusões for superior a 5000. E como esse será um processo longo, não posso me esforçar para bloquear essa tabela por um longo período de tempo. Se eu definir esse 60000 para 4500, você acha que obterá o mesmo desempenho?
cilerler
@cilerler Se você está preocupado com a escalação de bloqueios, pode desativá-lo no nível da tabela. Não há nada de errado em usar um tamanho de lote de 4500. A chave é que cada loop fará aproximadamente a mesma quantidade de trabalho.
Joe Obbish
Eu tenho que aceitar outra resposta devido às diferenças de velocidade. Testei sua solução e a solução da @ Martin-Smith e sua versão está obtendo mais dados ~ 2% em um teste de 10 minutos. Suas soluções são muito melhor do que a minha e eu realmente aprecio o seu tempo ... -
cilerler
2

Dois pensamentos vêm à mente:

O atraso provavelmente se deve à indexação com esse volume de dados. Tente soltar os índices, excluir e recriar os índices.

Ou..

Pode ser mais rápido copiar as linhas que você deseja manter em uma tabela temporária, soltar a tabela com 16 milhões de linhas e renomear a tabela temporária (ou copiar para uma nova instância da tabela de origem).

Jon
fonte