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).
WAITFOR DELAY
para meio segundo, mas isso é um trade-off com simultaneidade e, possivelmente, quanto é enviado via envio de log.Respondendo à segunda parte: como imprimir alguma saída durante o loop.
Eu tenho alguns procedimentos de manutenção de longa execução que o administrador do sistema às vezes precisa executar.
Eu os executo do SSMS e também notei que a
PRINT
instrução é mostrada no SSMS somente após a conclusão de todo o procedimento.Então, eu estou usando
RAISERROR
com baixa gravidade:Estou usando o SQL Server 2008 Standard e o SSMS 2012 (11.0.3128.0). Aqui está um exemplo de trabalho completo para executar no SSMS:
Quando eu comento
RAISERROR
e deixo apenasPRINT
as mensagens na guia Mensagens no SSMS, aparecem somente após a conclusão do lote inteiro, após 6 segundos.Quando eu comento
PRINT
e uso,RAISERROR
as mensagens na guia Mensagens no SSMS aparecem sem aguardar 6 segundos, mas à medida que o loop avança.Curiosamente, quando eu uso os dois
RAISERROR
ePRINT
, vejo as duas mensagens. Primeiro vem a mensagem do primeiroRAISERROR
, depois demora 2 segundos, depois o primeiroPRINT
e o segundoRAISERROR
e assim por diante.Em outros casos, eu uso uma
log
tabela dedicada separada e simplesmente insiro uma linha na tabela com algumas informações que descrevem o estado atual e o registro de data e hora do processo de longa execução.Enquanto o longo processo é executado, periodicamente
SELECT
dalog
tabela para ver o que está acontecendo.Obviamente, isso tem uma certa sobrecarga, mas deixa um log (ou histórico de logs) que eu posso examinar no meu próprio ritmo mais tarde.
fonte
Você pode monitorá-lo de outra conexão com algo como:
para ver quanto resta fazer. Isso pode ser útil se um aplicativo estiver chamando o processo, em vez de executá-lo manualmente no SSMS ou similar, e precisar mostrar progresso: execute o processo principal de forma assíncrona (ou em outro encadeamento) e, em seguida, faça um loop chamando "quanto resta" "verificar todos os momentos até que a chamada assíncrona (ou thread) seja concluída.
Definir o nível de isolamento o mais relaxado possível significa que isso deve retornar em tempo razoável sem ficar atrás da transação principal devido a problemas de bloqueio. Isso pode significar que o valor retornado é um pouco impreciso, é claro, mas como um simples medidor de progresso, isso não deve importar.
fonte