Métodos para acelerar um enorme DELETE FROM <table> sem cláusulas

37

Usando o SQL Server 2005.

Estou executando um enorme DELETE FROM sem cláusulas where. É basicamente equivalente a uma instrução TRUNCATE TABLE - exceto que eu não tenho permissão para usar TRUNCATE. O problema é que a tabela é enorme - 10 milhões de linhas e leva mais de uma hora para ser concluída. Existe alguma maneira de torná-lo mais rápido sem:

  • Usando Truncar
  • Desativando ou eliminando índices?

O log-t já está em um disco separado.

Todas as sugestões são bem-vindas!

tuseau
fonte
2
Se você vai fazer este lote um, considere dividindo a mesa
Gaius
11
Você não pode usar TRUNCATE porque existem restrições de FK referentes à tabela?
Nick Chammas

Respostas:

39

O que você pode fazer é excluir em lotes assim:

SELECT 'Starting' --sets @@ROWCOUNT
WHILE @@ROWCOUNT <> 0
    DELETE TOP (xxx) MyTable

Onde xxx é, digamos, 50000

Uma modificação disso, se você deseja remover uma porcentagem muito alta de linhas ...

SELECT col1, col2, ... INTO #Holdingtable
           FROM MyTable WHERE ..some condition..

SELECT 'Starting' --sets @@ROWCOUNT
WHILE @@ROWCOUNT <> 0
    DELETE TOP (xxx) MyTable WHERE ...

INSERT MyTable (col1, col2, ...)
           SELECT col1, col2, ... FROM #Holdingtable
gbn
fonte
3
@tuseau: cada exclusão requer algum espaço de log em caso de erro, para reverter. Uma exclusão de linha de 50k ocupa menos recursos / espaço do que a exclusão de linha de 10m. Obviamente, os backups de log ainda executam etc e ocupam espaço, mas é mais fácil no servidor para muitos lotes pequenos do que estragar um grande.
GBN
11
Obrigado, a exclusão do lote ajuda um pouco, acho que é a melhor opção.
tuseau
2
@ Phil Helmer: se a exclusão do lote estiver em uma transação, não haverá ganho em usá-la. Caso contrário, cada gravação de log é menor do que é, simplesmente, uma carga mais fácil
GBN
11
Mais um comentário: a exclusão do lote ajuda enormemente e leva 20 milhões de linhas a excluir de 1 hora 42 min a 3 min - MAS verifique se a tabela possui um índice em cluster! Se for um monte, a cláusula TOP cria uma classificação no plano de execução que nega qualquer melhoria. Parece óbvio depois.
usar o seguinte comando
2
@Noumenon: garante que @@ ROWCOUNT é de 1
gbn em
21

Você pode usar a cláusula TOP para fazer isso facilmente:

WHILE (1=1)
BEGIN
    DELETE TOP(1000) FROM table
    IF @@ROWCOUNT < 1 BREAK
END
SQLRockstar
fonte
Os colchetes formatam seu código
gbn 15/03
@gbn Isso está no SO. aqui ainda é 101 010.
bernd_k
7

Concordo com as sugestões de agrupar suas exclusões em partes gerenciáveis, se você não puder usar TRUNCATE, e eu gosto da sugestão de descartar / criar por sua originalidade, mas estou curioso sobre o seguinte comentário em sua pergunta:

É basicamente equivalente a uma instrução TRUNCATE TABLE - exceto que eu não tenho permissão para usar TRUNCATE

Suponho que o motivo dessa restrição tenha a ver com a segurança que precisa ser concedida para truncar diretamente uma tabela e o fato de permitir truncar tabelas diferentes daquela com a qual você está preocupado.

Supondo que seja esse o caso, estou pensando se ter um procedimento armazenado criado que use TRUNCATE TABLE e use "EXECUTE AS" seria considerado uma alternativa viável para fornecer os direitos de segurança necessários para truncar a tabela diretamente.

Felizmente, isso lhe daria a velocidade necessária e, ao mesmo tempo, resolvesse as preocupações de segurança que sua empresa pode ter ao adicionar sua conta à função db_ddladmin.

Outra vantagem de usar um procedimento armazenado dessa maneira é que o próprio procedimento armazenado pode ser bloqueado para que somente contas específicas possam usá-lo.

Se, por algum motivo, essa não for uma solução aceitável e sua necessidade de remover os dados desta tabela for algo que precise ser feito uma vez por dia / hora / etc, solicitei que um trabalho do SQL Agent fosse criado para truncar a tabela em um horário programado todos os dias.

Espero que isto ajude!

Jeff
fonte
5

Exceto truncar .. somente a exclusão em lotes pode ajudá-lo.

Você pode soltar a tabela e recriá-la, com todas as restrições e índices, fora do curso. No Management Studio, você tem a opção de criar um script para soltar e criar uma tabela, portanto deve ser uma opção trivial. Mas isso somente se você tiver permissão para executar ações DDL, o que vejo que não é realmente uma opção.

Marian
fonte
Como o aplicativo foi projetado para operações simultâneas, alterar a estrutura (DDL) e usar truncar não são opções ... Acho que a exclusão em lote é a melhor disponível. Obrigado embora.
tuseau
1

Como essa pergunta é uma referência tão importante, estou postando esse código que realmente me ajudou a entender a exclusão com loops e também as mensagens em um loop para acompanhar o progresso.

A consulta é modificada a partir desta pergunta duplicada. Crédito para @RLF pela base de consulta.

CREATE TABLE #DelTest (ID INT IDENTITY, name NVARCHAR(128)); -- Build the test table
INSERT INTO #DelTest (name) SELECT name FROM sys.objects;  -- fill from system DB
SELECT COUNT(*) TableNamesContainingSys FROM #deltest WHERE name LIKE '%sys%'; -- check rowcount
go
DECLARE @HowMany INT;
DECLARE @RowsTouched INT;
DECLARE @TotalRowCount INT;
DECLARE @msg VARCHAR(100);
DECLARE @starttime DATETIME 
DECLARE @currenttime DATETIME 

SET @RowsTouched = 1; -- Needs to be >0 for loop to start
SET @TotalRowCount=0  -- Total rows deleted so far is 0
SET @HowMany = 5;     -- Variable to choose how many rows to delete per loop
SET @starttime=GETDATE()

WHILE @RowsTouched > 0
BEGIN
   DELETE TOP (@HowMany)
   FROM #DelTest 
   WHERE name LIKE '%sys%';

   SET @RowsTouched = @@ROWCOUNT; -- Rows deleted this loop
   SET @TotalRowCount = @TotalRowCount+@RowsTouched; -- Increment Total rows deleted count
   SET @currenttime = GETDATE();
   SELECT @msg='Deleted ' + CONVERT(VARCHAR(9),@TotalRowCount) + ' Records. Runtime so far is '+CONVERT(VARCHAR(30),DATEDIFF(MILLISECOND,@starttime,@currenttime))+' milliseconds.'
   RAISERROR(@msg, 0, 1) WITH NOWAIT;  -- Print message after every loop. Can't use the PRINT function as SQL buffers output in loops.  

END; 
SELECT COUNT(*) TableNamesContainingSys FROM #DelTest WHERE name LIKE '%sys%'; -- Check row count after loop finish
DROP TABLE #DelTest;
XaM máximo
fonte