UPDATE performance onde nenhum dado é alterado

31

Se eu tiver uma UPDATEdeclaração que realmente não altere nenhum dado (porque os dados já estão no estado atualizado). Existe algum benefício de desempenho ao colocar uma verificação na WHEREcláusula para impedir a atualização?

Por exemplo, haveria alguma diferença na velocidade de execução entre UPDATE 1 e UPDATE 2 no seguinte:

CREATE TABLE MyTable (ID int PRIMARY KEY, Value int);
INSERT INTO MyTable (ID, Value)
VALUES
    (1, 1),
    (2, 2),
    (3, 3);

-- UPDATE 1
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2
    AND Value <> 2;
SELECT @@ROWCOUNT;

-- UPDATE 2
UPDATE MyTable
SET
    Value = 2
WHERE
    ID = 2;
SELECT @@ROWCOUNT;

DROP TABLE MyTable;

O motivo pelo qual pergunto é que preciso que a contagem de linhas inclua a linha inalterada, para que eu saiba se deve fazer uma inserção se o ID não existir. Como tal, usei o formulário UPDATE 2. Se houver um benefício de desempenho ao usar o formulário UPDATE 1, é possível obter a contagem de linhas necessária de alguma forma?

Martin Brown
fonte
Consulte sqlperformance.com/2012/10/t-sql-queries/conditional-updates (embora eu não tenha perfilado o caso em que nenhum valor é alterado).
Aaron Bertrand

Respostas:

24

Se eu tiver uma instrução UPDATE que realmente não altera nenhum dado (porque os dados já estão no estado atualizado), há algum benefício no desempenho ao marcar a cláusula where para impedir a atualização?

Certamente, pode haver uma pequena diferença de desempenho devido à ATUALIZAÇÃO 1 :

  • na verdade, não atualiza nenhuma linha (portanto, nada para gravar no disco, nem mesmo a atividade mínima de log) e
  • remover bloqueios menos restritivos do que o necessário para fazer a atualização real (portanto, melhor para a simultaneidade) ( consulte a seção Atualização no final )

No entanto, qual a diferença existente precisaria ser medida por você em seu sistema com seu esquema, dados e carga do sistema. Existem vários fatores que influenciam o impacto de uma atualização não atualizada:

  • a quantidade de contenção na tabela que está sendo atualizada
  • o número de linhas sendo atualizadas
  • se houver gatilhos UPDATE na tabela sendo atualizados (conforme observado por Mark em um comentário na pergunta). Se você executar UPDATE TableName SET Field1 = Field1, um acionador de atualização será acionado e indicará que o campo foi atualizado (se você marcar usando as funções UPDATE () ou COLUMNS_UPDATED ) e se o campo nas tabelas INSERTEDe DELETEDno mesmo é o mesmo valor.

Além disso, a seção de resumo a seguir é encontrada no artigo de Paul White, O impacto de atualizações não atualizáveis ​​(conforme observado por @spaghettidba em um comentário sobre sua resposta):

O SQL Server contém várias otimizações para evitar log desnecessário ou liberação de página ao processar uma operação UPDATE que não resultará em nenhuma alteração no banco de dados persistente.

  • As atualizações que não atualizam uma tabela em cluster geralmente evitam o log extra e a liberação da página, a menos que uma coluna que forma (parte da) chave do cluster seja afetada pela operação de atualização.
  • Se qualquer parte da chave do cluster for 'atualizada' para o mesmo valor, a operação será registrada como se os dados tivessem sido alterados e as páginas afetadas serão marcadas como sujas no buffer pool. Isso é uma conseqüência da conversão do UPDATE em uma operação de exclusão e inserção.
  • As tabelas de heap se comportam da mesma maneira que as tabelas em cluster, exceto que elas não possuem uma chave de cluster para causar log extra ou liberação de página. Esse permanece o caso, mesmo quando existe uma chave primária não agrupada no heap. Portanto, as atualizações não atualizadas em um heap geralmente evitam o log e a liberação extras (mas veja abaixo).
  • Os heaps e as tabelas em cluster sofrerão o log e a liberação extras para qualquer linha em que uma coluna LOB contendo mais de 8000 bytes de dados seja atualizada para o mesmo valor usando qualquer sintaxe diferente de 'SET column_name = column_name'.
  • A simples ativação de qualquer tipo de nível de isolamento de versão de linha em um banco de dados sempre causa o log e a liberação extras. Isso ocorre independentemente do nível de isolamento em vigor para a transação de atualização.

Lembre-se (especialmente se você não seguir o link para ver o artigo completo de Paulo), os dois itens a seguir:

  1. As atualizações não atualizadas ainda possuem alguma atividade de log, mostrando que uma transação está começando e terminando. Acontece que nenhuma modificação de dados acontece (o que ainda é uma boa economia).

  2. Como afirmei acima, você precisa testar no seu sistema. Use as mesmas consultas de pesquisa que Paul está usando e veja se você obtém os mesmos resultados. Estou vendo resultados ligeiramente diferentes no meu sistema do que o mostrado no artigo. Ainda não há páginas sujas a serem gravadas, mas um pouco mais de atividade de log.


... Preciso que a contagem de linhas inclua a linha inalterada, para saber se é necessário inserir se o ID não existe. ... é possível obter a contagem de linhas de que preciso de alguma forma?

Simplificando, se você está apenas lidando com uma única linha, pode fazer o seguinte:

UPDATE MyTable
SET    Value = 2
WHERE  ID = 2
AND Value <> 2;

IF (@@ROWCOUNT = 0)
BEGIN
  IF (NOT EXISTS(
                 SELECT *
                 FROM   MyTable
                 WHERE  ID = 2 -- or Value = 2 depending on the scenario
                )
     )
  BEGIN
     INSERT INTO MyTable (ID, Value) -- or leave out ID if it is an IDENTITY
     VALUES (2, 2);
  END;
END;

Para várias linhas, você pode obter as informações necessárias para tomar essa decisão usando a OUTPUTcláusula Ao capturar exatamente quais linhas foram atualizadas, é possível restringir os itens para procurar a diferença entre não atualizar linhas que não existem, em vez de não atualizar linhas que existem, mas que não precisam da atualização.

Eu mostro a implementação básica na seguinte resposta:

Como evitar o uso de consulta de mesclagem ao converter vários dados usando o parâmetro xml?

O método mostrado nessa resposta não filtra as linhas existentes, mas não precisa ser atualizado. Essa parte pode ser adicionada, mas você primeiro precisa mostrar exatamente onde está obtendo seu conjunto de dados no qual está se mesclando MyTable. Eles vêm de uma mesa temporária? Um parâmetro com valor de tabela (TVP)?


ATUALIZAÇÃO 1:

Finalmente pude fazer alguns testes e eis o que encontrei em relação ao bloqueio e ao log de transações. Primeiro, o esquema da tabela:

CREATE TABLE [dbo].[Test]
(
  [ID] [int] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED,
  [StringField] [varchar](500) NULL
);

Em seguida, o teste atualizando o campo para o valor que ele já possui:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117

Resultados:

-- Transaction Log (2 entries):
Operation
----------------------------
LOP_BEGIN_XACT
LOP_COMMIT_XACT


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
8 - IX          6 - PAGE
5 - X           7 - KEY

Por fim, o teste que filtra a atualização devido ao valor não mudar:

UPDATE rt
SET    rt.StringField = '04CF508B-B78E-4264-B9EE-E87DC4AD237A'
FROM   dbo.Test rt
WHERE  rt.ID = 4082117
AND    rt.StringField <> '04CF508B-B78E-4264-B9EE-E87DC4AD237A';

Resultados:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (3 Lock:Acquired events):
Mode            Type
--------------------------------------
8 - IX          5 - OBJECT
7 - IU          6 - PAGE
4 - U           7 - KEY

Como você pode ver, nada é gravado no log de transações ao filtrar a linha, em oposição às duas entradas que marcam o início e o final da transação. E embora seja verdade que essas duas entradas são quase nada, elas ainda são alguma coisa.

Além disso, o bloqueio dos recursos PAGE e KEY é menos restritivo ao filtrar as linhas que não foram alteradas. Se nenhum outro processo estiver interagindo com esta tabela, provavelmente não é um problema (mas qual é a probabilidade disso, realmente?). Lembre-se de que o teste mostrado em qualquer um dos blogs vinculados (e até mesmo no meu teste) pressupõe implicitamente que não há contenção na tabela, pois ela nunca faz parte dos testes. Dizer que as atualizações não atualizadas são tão leves que não vale a pena fazer a filtragem precisa ser feita com um pouco de sal, já que os testes foram feitos, mais ou menos, no vácuo. Mas em Produção, essa tabela provavelmente não está isolada. Obviamente, pode muito bem ser que o pouco de registro e bloqueios mais restritivos não se traduzam em menos eficiência. Então, a fonte mais confiável de informações para responder a essa pergunta? Servidor SQL. Especificamente:seu SQL Server. Ele mostrará qual método é melhor para o seu sistema :-).


ATUALIZAÇÃO 2:

Se as operações nas quais o novo valor é igual ao valor atual (ou seja, sem atualização) numeram as operações nas quais o novo valor é diferente e a atualização é necessária, o padrão a seguir pode ser ainda melhor, especialmente se há muita disputa na mesa. A idéia é fazer um simples SELECTprimeiro para obter o valor atual. Se você não obtiver um valor, terá sua resposta sobre o INSERT. Se você tiver um valor, poderá fazer um simples IFe emitir o UPDATE somente se for necessário.

DECLARE @CurrentValue VARCHAR(500) = NULL,
        @NewValue VARCHAR(500) = '04CF508B-B78E-4264-B9EE-E87DC4AD237A',
        @ID INT = 4082117;

SELECT @CurrentValue = rt.StringField
FROM   dbo.Test rt
WHERE  rt.ID = @ID;

IF (@CurrentValue IS NULL) -- if NULL is valid, use @@ROWCOUNT = 0
BEGIN
  -- row does not exist
  INSERT INTO dbo.Test (ID, StringField)
  VALUES (@ID, @NewValue);
END;
ELSE
BEGIN
  -- row exists, so check value to see if it is different
  IF (@CurrentValue <> @NewValue)
  BEGIN
    -- value is different, so do the update
    UPDATE rt
    SET    rt.StringField = @NewValue
    FROM   dbo.Test rt
    WHERE  rt.ID = @ID;
  END;
END;

Resultados:

-- Transaction Log (0 entries):
Operation
----------------------------


-- SQL Profiler (2 Lock:Acquired events):
Mode            Type
--------------------------------------
6 - IS          5 - OBJECT
6 - IS          6 - PAGE

Portanto, existem apenas dois bloqueios adquiridos em vez de 3, e ambos são Intent Shared, não Intent eXclusive ou Intent Update ( Compatibilidade de Bloqueios ). Tendo em mente que cada bloqueio adquirido também será liberado, cada bloqueio é realmente 2 operações, portanto, este novo método é um total de 4 operações em vez das 6 operações no método proposto originalmente. Considerando que esta operação está sendo executada uma vez a cada 15 ms (aproximadamente, conforme declarado pelo OP), ou seja, cerca de 66 vezes por segundo. Portanto, a proposta original equivale a 396 operações de bloqueio / desbloqueio por segundo, enquanto esse novo método equivale a apenas 264 operações de bloqueio / desbloqueio por segundo de bloqueios ainda mais leves. Isso não é garantia de um desempenho incrível, mas certamente vale a pena testar :-).

Solomon Rutzky
fonte
14

Diminua um pouco o zoom e pense na imagem maior. No mundo real, sua declaração de atualização será realmente assim:

UPDATE MyTable
  SET Value = 2
WHERE
     ID = 2
     AND Value <> 2;

Ou será mais parecido com isto:

UPDATE Customers
  SET AddressLine1 = '123 Main St',
      AddressLine2 = 'Apt 24',
      City = 'Chicago',
      State = 'IL',
      (and a couple dozen more fields)
WHERE
     ID = 2
     AND (AddressLine1 <> '123 Main St'
     OR AddressLine2 <> 'Apt 24'
     OR City <> 'Chicago'
     OR State <> 'IL'
      (and a couple dozen more fields))

Porque no mundo real, as tabelas têm muitas colunas. Isso significa que você terá que gerar muita lógica complexa de aplicativo dinâmico para criar seqüências dinâmicas, OU precisará especificar sempre o conteúdo antes e depois de cada campo.

Se você criar essas instruções de atualização dinamicamente para todas as tabelas, passando apenas nos campos que estão sendo atualizados, poderá encontrar rapidamente um problema de poluição do cache do plano semelhante ao problema de tamanhos de parâmetro do NHibernate, alguns anos atrás. Pior ainda, se você criar as instruções de atualização no SQL Server (como nos procedimentos armazenados), queimará preciosos ciclos da CPU porque o SQL Server não é muito eficiente na concatenação de seqüências de caracteres em escala.

Devido a essas complexidades, geralmente não faz sentido fazer esse tipo de comparação linha por linha, campo a campo, enquanto você faz as atualizações. Pense em operações baseadas em conjuntos.

Brent Ozar
fonte
11
Meu exemplo do mundo real é tão simples quanto isso, mas é muito chamado. Minha estimativa é uma vez a cada 15ms nos horários de pico. Eu queria saber se o SQL Server é cutelo o suficiente para não gravar no disco quando não é necessário.
Martin Brown
3

Você pode ver um ganho de desempenho ao ignorar linhas que não precisam ser atualizadas apenas quando o número de linhas é grande (menos log, menos páginas sujas para gravar no disco).

Ao lidar com atualizações de linha única, como no seu caso, a diferença de desempenho é completamente insignificante. Se a atualização das linhas em todos os casos facilitar para você, faça-o.

Para obter mais informações sobre o tópico, consulte Atualizações sem atualização por Paul White

spaghettidba
fonte
3

Você pode combinar a atualização e inserir em uma instrução. No SQL Server, você pode usar uma instrução MERGE para fazer a atualização e a inserção, se não for encontrada. Para o MySQL, você pode usar INSERT ON DUPLICATE KEY UPDATE .

Russell Harkins
fonte
1

Em vez de verificar os valores de todos os campos, você não pode obter um valor de hash usando as colunas nas quais está interessado e comparar com o hash armazenado na linha da tabela?

IF EXISTS (Select 1 from Table where ID =@ID AND HashValue=Sha256(column1+column2))
GOTO EXIT
ELSE
Ruchira Liyanagama
fonte