Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

Como atualizar uma tabela grande com milhões de linhas no SQL Server?


  1. Você não deve atualizar 10 mil linhas em um conjunto, a menos que tenha certeza de que a operação está obtendo bloqueios de página (devido a várias linhas por página serem parte do UPDATE Operação). O problema é que o escalonamento de bloqueio (de bloqueios de linha ou página para tabela) ocorre em 5.000 bloqueios . Portanto, é mais seguro mantê-lo abaixo de 5000, caso a operação esteja usando Row Locks.

  2. Você não estar usando SET ROWCOUNT para limitar o número de linhas que serão modificadas. Há duas questões aqui:

    1. Ele foi preterido desde que o SQL Server 2005 foi lançado (11 anos atrás):

      O uso de SET ROWCOUNT não afetará as instruções DELETE, INSERT e UPDATE em uma versão futura do SQL Server. Evite usar SET ROWCOUNT com instruções DELETE, INSERT e UPDATE em novos trabalhos de desenvolvimento e planeje modificar os aplicativos que o utilizam atualmente. Para um comportamento semelhante, use a sintaxe TOP

    2. Pode afetar mais do que apenas a declaração com a qual você está lidando:

      Definir a opção SET ROWCOUNT faz com que a maioria das instruções Transact-SQL parem de processar quando forem afetadas pelo número especificado de linhas. Isso inclui gatilhos. A opção ROWCOUNT não afeta cursores dinâmicos, mas limita o conjunto de linhas de cursores de conjunto de chaves e insensíveis. Esta opção deve ser usada com cautela.

    Em vez disso, use o TOP () cláusula.

  3. Não há nenhum propósito em ter uma transação explícita aqui. Isso complica o código e você não tem como lidar com um ROLLBACK , o que nem é necessário, pois cada instrução é sua própria transação (ou seja, auto-commit).

  4. Supondo que você encontre uma razão para manter a transação explícita, então você não tem um TRY / CATCH estrutura. Por favor, veja minha resposta no DBA.StackExchange para um TRY / CATCH modelo que lida com transações:

    Somos obrigados a lidar com a transação no código C #, bem como no procedimento da loja

Eu suspeito que o verdadeiro WHERE cláusula não está sendo mostrada no código de exemplo na pergunta, então simplesmente confiando no que foi mostrado, um melhor modelo seria:
DECLARE @Rows INT,
        @BatchSize INT; -- keep below 5000 to be safe
    
SET @BatchSize = 2000;

SET @Rows = @BatchSize; -- initialize just to enter the loop

BEGIN TRY    
  WHILE (@Rows = @BatchSize)
  BEGIN
      UPDATE TOP (@BatchSize) tab
      SET    tab.Value = 'abc1'
      FROM  TableName tab
      WHERE tab.Parameter1 = 'abc'
      AND   tab.Parameter2 = 123
      AND   tab.Value <> 'abc1' COLLATE Latin1_General_100_BIN2;
      -- Use a binary Collation (ending in _BIN2, not _BIN) to make sure
      -- that you don't skip differences that compare the same due to
      -- insensitivity of case, accent, etc, or linguistic equivalence.

      SET @Rows = @@ROWCOUNT;
  END;
END TRY
BEGIN CATCH
  RAISERROR(stuff);
  RETURN;
END CATCH;

Ao testar @Rows contra @BatchSize , você pode evitar esse UPDATE final query (na maioria dos casos) porque o conjunto final normalmente é um número de linhas menor que @BatchSize , caso em que sabemos que não há mais para processar (que é o que você vê na saída mostrada em sua resposta). Apenas nos casos em que o conjunto final de linhas é igual a @BatchSize este código executará um UPDATE final afetando 0 linhas.

Eu também adicionei uma condição ao WHERE cláusula para evitar que as linhas que já foram atualizadas sejam atualizadas novamente.

OBSERVAÇÃO SOBRE O DESEMPENHO

Eu enfatizei "melhor" acima (como em "este é um melhor model") porque isso tem várias melhorias em relação ao código original do O.P. e funciona bem em muitos casos, mas não é perfeito para todos os casos. Para tabelas de pelo menos um determinado tamanho (que varia devido a vários fatores, para ser mais específico), o desempenho será degradado, pois há menos linhas para corrigir se:
  1. não há índice para dar suporte à consulta ou
  2. há um índice, mas pelo menos uma coluna no WHERE cláusula é um tipo de dados string que não usa um agrupamento binário, portanto, um COLLATE é adicionada à consulta aqui para forçar o agrupamento binário, e isso invalida o índice (para essa consulta específica).

Esta é a situação que a @mikesigs encontrou, exigindo assim uma abordagem diferente. O método atualizado copia os IDs de todas as linhas a serem atualizadas em uma tabela temporária e, em seguida, usa essa tabela temporária para INNER JOIN para a tabela que está sendo atualizada na(s) coluna(s) de chave de índice clusterizado. (É importante capturar e ingressar no índice clusterizado colunas, sejam ou não as colunas de chave primária!).

Por favor, veja a resposta @mikesigs abaixo para mais detalhes. A abordagem mostrada nessa resposta é um padrão muito eficaz que eu mesmo usei em muitas ocasiões. As únicas mudanças que eu faria são:
  1. Crie explicitamente os #targetIds tabela em vez de usar SELECT INTO...
  2. Para os #targetIds table, declare uma chave primária clusterizada na(s) coluna(s).
  3. Para os #batchIds table, declare uma chave primária clusterizada na(s) coluna(s).
  4. Para inserir em #targetIds , use INSERT INTO #targetIds (column_name(s)) SELECT e remova o ORDER BY pois é desnecessário.

Portanto, se você não tiver um índice que possa ser usado para esta operação e não puder criar temporariamente um que realmente funcione (um índice filtrado pode funcionar, dependendo do seu WHERE cláusula para o UPDATE query), tente a abordagem mostrada na resposta @mikesigs (e se você usar essa solução, vote nela).