Eu não estava ciente dessa pergunta quando respondi à pergunta relacionada ( são necessárias transações explícitas neste loop while? ), Mas por uma questão de exaustividade, abordarei esse problema aqui, pois não fazia parte da minha sugestão nessa resposta vinculada. .
Como estou sugerindo agendar isso por meio de um trabalho do SQL Agent (afinal, são 100 milhões de linhas), não acho que qualquer forma de enviar mensagens de status para o cliente (ou seja, SSMS) seja ideal (embora seja isso sempre necessitando de outros projetos, concordo com Vladimir que usar RAISERROR('', 10, 1) WITH NOWAIT;
é o caminho a seguir).
Nesse caso em particular, eu criaria uma tabela de status que pode ser atualizada por cada loop com o número de linhas atualizadas até o momento. E não faz mal jogar no tempo atual para ter um batimento cardíaco no processo.
Como você deseja cancelar e reiniciar o processo, Estou cansado de agrupar o UPDATE da tabela principal com o UPDATE da tabela de status em uma transação explícita. No entanto, se você achar que a tabela de status está sempre fora de sincronia devido ao cancelamento, é fácil atualizar com o valor atual, simplesmente atualizando-o manualmente com o COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL
.e como existem duas tabelas para UPDATE (ou seja, a tabela principal e a tabela de status), devemos usar uma transação explícita para manter essas duas tabelas sincronizadas, mas não queremos arriscar uma transação órfã se você cancelar o processo a uma ponto após o início da transação, mas não a confirmou. Isso deve ser seguro, desde que você não pare o trabalho do SQL Agent.
Como você pode parar o processo sem, bem, pará-lo? Pedindo para parar :-). Sim. Enviando ao processo um "sinal" (semelhante ao kill -3
Unix), você pode solicitar que ele pare no próximo momento conveniente (ou seja, quando não houver transação ativa!) E faça com que ele se limpe de maneira agradável e organizada.
Como você pode se comunicar com o processo em execução em outra sessão? Usando o mesmo mecanismo que criamos para que ele comunique seu status atual de volta para você: a tabela de status. Só precisamos adicionar uma coluna que o processo verificará no início de cada loop, para que ele saiba se deve prosseguir ou abortar. E como a intenção é agendar isso como uma tarefa do SQL Agent (executada a cada 10 ou 20 minutos), também devemos verificar desde o início, pois não faz sentido preencher uma tabela temporária com 1 milhão de linhas se o processo está apenas indo sair um momento depois e não usar nenhum desses dados.
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;
Você pode verificar o status a qualquer momento usando a seguinte consulta:
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;
Deseja pausar o processo, esteja ele sendo executado em um trabalho do SQL Agent ou mesmo no SSMS no computador de outra pessoa? Apenas corra:
UPDATE ht
SET ht.PauseProcess = 1
FROM dbo.HugeTable_TempStatus ts;
Deseja que o processo possa iniciar novamente? Apenas corra:
UPDATE ht
SET ht.PauseProcess = 0
FROM dbo.HugeTable_TempStatus ts;
ATUALIZAR:
Aqui estão algumas coisas adicionais a serem tentadas que podem melhorar o desempenho desta operação. Não há garantia de ajuda alguma, mas provavelmente vale a pena testar. E com 100 milhões de linhas para atualizar, você tem bastante tempo / oportunidade para testar algumas variações ;-).
- Adicione
TOP (@UpdateRows)
à consulta UPDATE para que a linha superior se pareça:
UPDATE TOP (@UpdateRows) ht
Às vezes, ajuda o otimizador a saber quantas linhas máximas serão afetadas, para que não perca tempo procurando mais.
Adicione uma chave primária à #CurrentSet
tabela temporária. A idéia aqui é ajudar o otimizador a se juntar à tabela de 100 milhões de linhas.
E apenas para que seja declarado de forma a não ser ambíguo, não deve haver motivo para adicionar uma PK à #FullSet
tabela temporária, pois é apenas uma tabela de fila simples em que a ordem é irrelevante.
- Em alguns casos, ajuda a adicionar um Índice Filtrado para ajudar os
SELECT
que são alimentados na #FullSet
tabela temporária. Aqui estão algumas considerações relacionadas à adição desse índice:
- A condição WHERE deve corresponder à condição WHERE da sua consulta, portanto
WHERE deleted is null or deletedDate is null
- No início do processo, a maioria das linhas corresponderá à sua condição WHERE, portanto, um índice não é tão útil. Você pode esperar até algo em torno da marca de 50% antes de adicionar isso. Obviamente, quanto ajuda e quando é melhor adicionar o índice variam devido a vários fatores, por isso é um pouco de tentativa e erro.
- Talvez seja necessário atualizar manualmente STATS e / ou RECONSTRUIR o índice para mantê-lo atualizado, pois os dados base estão mudando com bastante frequência
- Lembre-se de que o índice, enquanto ajuda o
SELECT
, prejudicará o, UPDATE
pois é outro objeto que deve ser atualizado durante essa operação, portanto, mais E / S. Isso se aplica tanto ao uso de um índice filtrado (que diminui à medida que você atualiza as linhas, pois menos linhas correspondem ao filtro), quanto à espera de adicionar um índice (se não for de grande ajuda no início, não há razão para incorrer). E / S adicional).