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 INSERTED
e DELETED
no 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:
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).
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 OUTPUT
clá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 SELECT
primeiro 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 IF
e 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 :-).