As transações explícitas são necessárias nesse loop while?

11

SQL Server 2014:

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

Para o envio de logs, etc, também queremos, obviamente, mantê-lo em transações menores.

Se deixarmos o abaixo executar um pouco e, em seguida, cancelar / encerrar a consulta, o trabalho realizado até agora será confirmado ou será necessário adicionar instruções explícitas BEGIN TRANSACTION / END TRANSACTION para que possamos cancelar a qualquer momento?

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:

13

Instruções individuais - DML, DDL, etc - são transações em si mesmas. Então, sim, após cada iteração do loop (tecnicamente: após cada instrução), qualquer que seja a UPDATEalteração dessa declaração foi confirmada automaticamente.

Claro, sempre há uma exceção, certo? É possível ativar transações implícitas via SET IMPLICIT_TRANSACTIONS ; nesse caso, a primeira UPDATEinstrução iniciaria uma transação que você precisaria para COMMITou ROLLBACKno final. Essa é uma configuração no nível da sessão que está desativada por padrão na maioria dos casos.

precisamos adicionar instruções explícitas de BEGIN TRANSACTION / END TRANSACTION para que possamos cancelar a qualquer momento?

Não. E, de fato, considerando que você deseja interromper o processo e reiniciar, adicionar uma transação explícita (ou ativar transações implícitas) seria uma má ideia, pois interromper o processo pode pegá-lo antes de ele fazer o COMMIT. Nesse caso, você precisaria emitir manualmente o COMMIT(se você estiver no SSMS) ou se estiver executando isso em um trabalho do SQL Agent, não terá essa oportunidade e poderá acabar com uma transação órfã.


Além disso, convém definir @CHUNK_SIZEum número menor. A escalação de bloqueios geralmente ocorre em 5000 bloqueios adquiridos em um único objeto. Dependendo do tamanho das linhas e se estiver fazendo bloqueios de linha versus bloqueios de página, você pode estar ultrapassando esse limite. Se o tamanho de uma linha for de tal forma que apenas 1 ou 2 linhas se ajustem a cada página, você poderá sempre acessá-lo, mesmo que esteja bloqueando a página.

Se a tabela estiver particionada, você poderá definir a LOCK_ESCALATIONopção (introduzida no SQL Server 2008) para que a tabela AUTObloqueie apenas a partição e não a tabela inteira ao ser escalada. Ou, para qualquer tabela, você pode definir a mesma opção DISABLE, embora tenha que ter muito cuidado com isso. Veja ALTER TABLE para detalhes.

Aqui está uma documentação que fala sobre o escalonamento de bloqueios e os limites: Escalonamento de bloqueios (diz se aplica ao "SQL Server 2008 R2 e versões posteriores"). E aqui está uma postagem de blog que trata da detecção e correção da escalação de bloqueios: Bloqueio no Microsoft SQL Server (Parte 12 - Escalação de Bloqueios) .


Independentemente da pergunta exata, mas relacionada à consulta na pergunta, existem algumas melhorias que podem ser feitas aqui (ou pelo menos parece assim apenas ao olhar para ela):

  1. Para o seu loop, fazer WHILE (@@ROWCOUNT = @CHUNK_SIZE)é um pouco melhor, pois se o número de linhas atualizadas na última iteração for menor que o valor solicitado para UPDATE, não haverá trabalho a ser feito.

  2. Se o deletedcampo é um BITtipo de dados, então não é que o valor determinado por se ou não deletedDateé 2000-01-01? Por que você precisa dos dois?

  3. Se esses dois campos são novos e você os adicionou NULL, pode ser uma operação on-line / sem bloqueio e agora deseja atualizá-los para o valor "padrão", então isso não era necessário. A partir do SQL Server 2012 (apenas Enterprise Edition), a adição de NOT NULLcolunas com restrição DEFAULT é uma operação sem bloqueio, desde que o valor de DEFAULT seja constante. Portanto, se você ainda não estiver usando os campos, basta soltar e adicionar novamente como NOT NULLe com uma restrição DEFAULT.

  4. Se nenhum outro processo estiver atualizando esses campos enquanto você estiver fazendo essa atualização, será mais rápido se você enfileirar os registros que deseja atualizar e apenas trabalhar nessa fila. Há um problema de desempenho no método atual, pois é necessário consultar novamente a tabela a cada vez para obter o conjunto que precisa ser alterado. Em vez disso, você pode fazer o seguinte, que varre a tabela apenas uma vez nesses dois campos e emite apenas instruções UPDATE muito direcionadas. Também não há penalidade de interromper o processo a qualquer momento e iniciá-lo mais tarde, pois a população inicial da fila simplesmente encontrará os registros restantes para atualização.

    1. Crie uma tabela temporária (#FullSet) que possui apenas os campos-chave do índice clusterizado.
    2. Crie uma segunda tabela temporária (#CurrentSet) dessa mesma estrutura.
    3. inserir em #FullSet via SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;

      O TOP(n)está lá devido ao tamanho da tabela. Com 100 milhões de linhas na tabela, você realmente não precisa preencher a tabela de filas com todo esse conjunto de chaves, especialmente se planeja interromper o processo de vez em quando e reiniciá-lo mais tarde. Então, talvez seja definido ncomo 1 milhão e deixe isso terminar até o fim. Você sempre pode agendar isso em um trabalho do SQL Agent que execute o conjunto de 1 milhão (ou talvez até menos) e aguarde o próximo horário agendado para recuperar novamente. Em seguida, você pode agendar a execução a cada 20 minutos, para que haja algum espaço de respiração forçado entre os conjuntos de n, mas ele ainda concluirá todo o processo sem supervisão. Depois, basta que o trabalho se exclua quando não houver mais nada a fazer :-).

    4. em um loop, faça:
      1. Preencha o lote atual por meio de algo como DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. Faça o UPDATE usando algo como: UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;
      4. Limpe o conjunto atual: TRUNCATE TABLE #CurrentSet;
  5. 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. Pode ser 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 funciona tanto no uso de um Índice Filtrado (que diminui à medida que você atualiza as linhas, pois menos linhas correspondem ao filtro) quanto na 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).

ATUALIZAÇÃO: Por favor, veja minha resposta a uma pergunta relacionada a esta questão para a implementação completa do que é sugerido acima, incluindo um mecanismo para rastrear o status e cancelar de forma limpa: servidor sql: atualizando campos em uma tabela enorme em pequenos pedaços: como obter status de progresso?

Solomon Rutzky
fonte
Suas sugestões no número 4 podem ser mais rápidas em alguns casos, mas isso parece uma complexidade de código significativa a ser adicionada. Eu preferiria começar de maneira simples e, se isso não atender às suas necessidades, considere alternativas.
Bacon Bits
@BaconBits Concordou em começar de forma simples. Para ser justo, essas sugestões não foram aplicadas a todos os cenários. A questão é sobre como lidar com uma tabela muito grande (100 milhões de linhas).
Solomon Rutzky