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

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).

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.


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.