servidor sql: atualizando campos em uma tabela enorme em pequenos pedaços: como obter progresso / status?

10

Temos uma tabela muito grande (100 milhões de linhas) e precisamos atualizar alguns campos nela.

Para envio de logs, etc, também queremos, obviamente, mantê-lo nas transações de tamanho reduzido.

  • O abaixo fará o truque?
  • E como podemos imprimir algumas saídas, para que possamos ver o progresso? (tentamos adicionar uma instrução PRINT lá, mas nada foi produzido durante o loop while)

O código é:

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END
Jonesome Restabelecer Monica
fonte

Respostas:

12

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 -3Unix), 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 ;-).

  1. 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.
  2. Adicione uma chave primária à #CurrentSettabela 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 à #FullSettabela temporária, pois é apenas uma tabela de fila simples em que a ordem é irrelevante.

  3. Em alguns casos, ajuda a adicionar um Índice Filtrado para ajudar os SELECTque são alimentados na #FullSettabela temporária. Aqui estão algumas considerações relacionadas à adição desse índice:
    1. A condição WHERE deve corresponder à condição WHERE da sua consulta, portanto WHERE deleted is null or deletedDate is null
    2. 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.
    3. 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
    4. Lembre-se de que o índice, enquanto ajuda o SELECT, prejudicará o, UPDATEpois é 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).
Solomon Rutzky
fonte
11
Isto e excelente. Estou executando agora, e parece que podemos executá-lo on-line, durante o dia. Obrigado!
Jonesome Reinstate Monica
@samsmith Por favor, veja a seção UPDATE que acabei de adicionar, pois existem algumas idéias para potencialmente tornar o processo ainda mais rápido.
Solomon Rutzky
Sem os aprimoramentos do UPDATE, estamos recebendo cerca de 8 milhões de atualizações / hora ... com o @BatchRows definido como 10000000 (dez milhões) #
Jonesome Reinstate Monica
@samsmith Isso é ótimo :) certo? Tenha em mente duas coisas: 1) O processo vai abrandar, pois há cada vez menos as linhas correspondentes à cláusula WHERE, portanto, por isso que seria um bom momento para adicionar um índice filtrado, mas você já adicionou um índice não-filtrada na comece, então não tenho certeza se isso vai ajudar ou prejudicar, mas ainda assim esperaria que a taxa de transferência diminuísse à medida que se aproximasse do processo; e 2) você pode aumentar a taxa de transferência reduzindo WAITFOR DELAYpara meio segundo, mas isso é um trade-off com simultaneidade e, possivelmente, quanto é enviado via envio de log.
Solomon Rutzky
Estamos felizes com 8 milhões de linhas / hora. Sim, podemos vê-lo desacelerando. Estamos hesitantes em criar mais índices (porque a tabela está bloqueada para toda a compilação). O que fizemos algumas vezes é reorganizar o índice existente (porque isso está on-line).
Jonesome Reinstate Monica
4

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 PRINTinstrução é mostrada no SSMS somente após a conclusão de todo o procedimento.

Então, eu estou usando RAISERRORcom baixa gravidade:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

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:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

Quando eu comento RAISERRORe deixo apenas PRINTas mensagens na guia Mensagens no SSMS, aparecem somente após a conclusão do lote inteiro, após 6 segundos.

Quando eu comento PRINTe uso, RAISERRORas mensagens na guia Mensagens no SSMS aparecem sem aguardar 6 segundos, mas à medida que o loop avança.

Curiosamente, quando eu uso os dois RAISERRORe PRINT, vejo as duas mensagens. Primeiro vem a mensagem do primeiro RAISERROR, depois demora 2 segundos, depois o primeiro PRINTe o segundo RAISERRORe assim por diante.


Em outros casos, eu uso uma logtabela 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 SELECTda logtabela 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.

Vladimir Baranov
fonte
No SQL 2008/2014, não podemos ver os resultados do raiserror .... o que estamos perdendo?
Jonesome Reinstate Monica
@samsmith, adicionei um exemplo completo. Tente. Que comportamento você tem neste exemplo simples?
Vladimir Baranov
2

Você pode monitorá-lo de outra conexão com algo como:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

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.

David Spillett
fonte