Database
 sql >> Base de Dados >  >> RDS >> Database

T-SQL terça-feira #64:Um gatilho ou muitos?




É aquela terça-feira do mês – você sabe, aquela em que acontece a festa do bloco de blogueiros conhecida como T-SQL Tuesday. Este mês é apresentado por Russ Thomas (@SQLJudo), e o tópico é "Chamando todos os sintonizadores e cabeças de engrenagem". Vou tratar de um problema relacionado ao desempenho aqui, embora me desculpe por não estar totalmente de acordo com as diretrizes que Russ estabeleceu em seu convite (não vou usar dicas, sinalizadores de rastreamento ou guias de plano) .

No SQLBits na semana passada, fiz uma apresentação sobre gatilhos, e meu bom amigo e colega MVP Erland Sommarskog compareceu. A certa altura, sugeri que antes de criar um novo gatilho em uma tabela, você deve verificar se já existe algum gatilho e considerar combinar a lógica em vez de adicionar um gatilho adicional. Minhas razões foram principalmente para a manutenção do código, mas também para o desempenho. Erland perguntou se eu já havia testado para ver se havia alguma sobrecarga adicional em ter vários gatilhos disparados para a mesma ação, e eu tive que admitir que, não, eu não tinha feito nada extenso. Então eu vou fazer isso agora.

No AdventureWorks2014, criei um conjunto simples de tabelas que basicamente representam sys.all_objects (~2.700 linhas) e sys.all_columns (~9.500 linhas). Eu queria medir o efeito na carga de trabalho de várias abordagens para atualizar ambas as tabelas – essencialmente, você tem usuários atualizando a tabela de colunas e usa um gatilho para atualizar uma coluna diferente na mesma tabela e algumas colunas na tabela de objetos.
  • T1:linha de base :Suponha que você possa controlar todo o acesso a dados por meio de um procedimento armazenado; neste caso, as atualizações em ambas as tabelas podem ser realizadas diretamente, sem a necessidade de triggers. (Isso não é prático no mundo real, porque você não pode proibir de forma confiável o acesso direto às tabelas.)
  • T2:gatilho único contra outra tabela :Suponha que você possa controlar a instrução de atualização na tabela afetada e adicionar outras colunas, mas as atualizações na tabela secundária precisam ser implementadas com um gatilho. Atualizaremos todas as três colunas com uma instrução.
  • T3:gatilho único em ambas as tabelas :nesse caso, temos um gatilho com duas instruções, uma que atualiza a outra coluna na tabela afetada e outra que atualiza todas as três colunas na tabela secundária.
  • T4:gatilho único em ambas as tabelas :Como T3, mas desta vez, temos um gatilho com quatro instruções, uma que atualiza a outra coluna na tabela afetada e uma instrução para cada coluna atualizada na tabela secundária. Essa pode ser a maneira como isso é tratado se os requisitos forem adicionados ao longo do tempo e uma instrução separada for considerada mais segura em termos de teste de regressão.
  • T5:dois acionadores :Um gatilho atualiza apenas a tabela afetada; o outro usa uma única instrução para atualizar as três colunas na tabela secundária. Isso pode ser feito se os outros acionadores não forem percebidos ou se for proibido modificá-los.
  • T6:quatro acionadores :Um gatilho atualiza apenas a tabela afetada; os outros três atualizam cada coluna na tabela secundária. Novamente, isso pode ser feito se você não souber que os outros gatilhos existem ou se tiver medo de tocar nos outros gatilhos devido a problemas de regressão.

Aqui estão os dados de origem com os quais estamos lidando:
-- sys.all_objects:
SELECT * INTO dbo.src FROM sys.all_objects;
CREATE UNIQUE CLUSTERED INDEX x ON dbo.src([object_id]);
GO
 
-- sys.all_columns:
SELECT * INTO dbo.tr1 FROM sys.all_columns;
CREATE UNIQUE CLUSTERED INDEX x ON dbo.tr1([object_id], column_id);
-- repeat 5 times: tr2, tr3, tr4, tr5, tr6

Agora, para cada um dos 6 testes, vamos executar nossas atualizações 1.000 vezes e medir o tempo

T1:linha de base


Este é o cenário em que temos a sorte de evitar gatilhos (novamente, não muito realistas). Nesse caso, mediremos as leituras e a duração desse lote. Eu coloquei /*real*/ no texto da consulta para que eu possa facilmente extrair as estatísticas apenas para essas instruções, e não para quaisquer instruções de dentro dos gatilhos, pois, em última análise, as métricas se acumulam nas instruções que invocam os gatilhos. Observe também que as atualizações reais que estou fazendo não fazem muito sentido, então ignore que estou definindo o agrupamento para o nome do servidor/instância e o principal_id do objeto para o session_id da sessão atual .
UPDATE /*real*/ dbo.tr1 SET name += N'',
  collation_name = @@SERVERNAME
  WHERE name LIKE '%s%';
 
UPDATE /*real*/ s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID
  FROM dbo.src AS s
  INNER JOIN dbo.tr1 AS t
  ON s.[object_id] = t.[object_id]
  WHERE t.name LIKE '%s%';
 
GO 1000

T2:gatilho único


Para isso, precisamos do seguinte gatilho simples, que atualiza apenas dbo.src :
CREATE TRIGGER dbo.tr_tr2
ON dbo.tr2
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = SUSER_ID()
    FROM dbo.src AS s 
	INNER JOIN inserted AS i
	ON s.[object_id] = i.[object_id];
END
GO

Então nosso lote só precisa atualizar as duas colunas na tabela primária:
UPDATE /*real*/ dbo.tr2 SET name += N'', collation_name = @@SERVERNAME
  WHERE name LIKE '%s%';
GO 1000

T3:gatilho único nas duas tabelas


Para este teste, nosso gatilho se parece com isso:
CREATE TRIGGER dbo.tr_tr3
ON dbo.tr3
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr3 AS t
	INNER JOIN inserted AS i
	ON t.[object_id] = i.[object_id];
 
  UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

E agora o lote que estamos testando precisa apenas atualizar a coluna original na tabela primária; o outro é tratado pelo gatilho:
UPDATE /*real*/ dbo.tr3 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T4:gatilho único nas duas tabelas


Isso é como T3, mas agora o gatilho tem quatro instruções:
CREATE TRIGGER dbo.tr_tr4
ON dbo.tr4
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr4 AS t
	INNER JOIN inserted AS i
	ON t.[object_id] = i.[object_id];
 
  UPDATE s SET modify_date = GETDATE()
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
 
  UPDATE s SET is_ms_shipped = 0
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
 
  UPDATE s SET principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

O lote de teste permanece inalterado:
UPDATE /*real*/ dbo.tr4 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T5:dois acionadores


Aqui temos um gatilho para atualizar a tabela primária e um gatilho para atualizar a tabela secundária:
CREATE TRIGGER dbo.tr_tr5_1
ON dbo.tr5
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr5 AS t
	INNER JOIN inserted AS i
	ON t.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr5_2
ON dbo.tr5
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET modify_date = GETDATE(), is_ms_shipped = 0, principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

O lote de teste é novamente muito básico:
UPDATE /*real*/ dbo.tr5 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T6:quatro acionadores


Desta vez temos um gatilho para cada coluna afetada; um na tabela primária e três nas tabelas secundárias.
CREATE TRIGGER dbo.tr_tr6_1
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE t SET collation_name = @@SERVERNAME
    FROM dbo.tr6 AS t
    INNER JOIN inserted AS i
    ON t.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr6_2
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET modify_date = GETDATE()
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr6_3
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET is_ms_shipped = 0
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO
 
CREATE TRIGGER dbo.tr_tr6_4
ON dbo.tr6
AFTER UPDATE
AS
BEGIN
  SET NOCOUNT ON;
  UPDATE s SET principal_id = @@SPID
    FROM dbo.src AS s
    INNER JOIN inserted AS i
    ON s.[object_id] = i.[object_id];
END
GO

E o lote de teste:
UPDATE /*real*/ dbo.tr6 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

Medindo o impacto da carga de trabalho


Por fim, escrevi uma consulta simples em sys.dm_exec_query_stats para medir as leituras e a duração de cada teste:
SELECT 
  [cmd] = SUBSTRING(t.text, CHARINDEX(N'U', t.text), 23), 
  avg_elapsed_time = total_elapsed_time / execution_count * 1.0,
  total_logical_reads
FROM sys.dm_exec_query_stats AS s 
CROSS APPLY sys.dm_exec_sql_text(s.sql_handle) AS t
WHERE t.text LIKE N'%UPDATE /*real*/%'
ORDER BY cmd;

Resultados


Fiz os testes 10 vezes, coletei os resultados e fiz a média de tudo. Aqui está como ele quebrou:
Teste/Lote Duração média
(microssegundos)
Total de leituras
(8 mil páginas)
T1 :UPDATE /*real*/ dbo.tr1 … 22.608 205.134
T2 :UPDATE /*real*/ dbo.tr2 … 32.749 11.331.628
T3 :UPDATE /*real*/ dbo.tr3 … 72.899 22.838.308
T4 :UPDATE /*real*/ dbo.tr4 … 78.372 44.463.275
T5 :UPDATE /*real*/ dbo.tr5 … 88.563 41.514.778
T6 :UPDATE /*real*/ dbo.tr6 … 127.079 100.330.753


E aqui está uma representação gráfica da duração:


Conclusão


É claro que, nesse caso, há uma sobrecarga substancial para cada gatilho invocado – todos esses lotes afetaram o mesmo número de linhas, mas em alguns casos as mesmas linhas foram tocadas várias vezes. Provavelmente realizarei mais testes de acompanhamento para medir a diferença quando a mesma linha nunca é tocada mais de uma vez - um esquema mais complicado, talvez, onde 5 ou 10 outras tabelas precisam ser tocadas todas as vezes, e essas instruções diferentes podem ser em um único gatilho ou em vários. Meu palpite é que as diferenças de sobrecarga serão impulsionadas mais por coisas como simultaneidade e o número de linhas afetadas do que pela sobrecarga do próprio gatilho - mas veremos.

Quer experimentar a demonstração você mesmo? Baixe o roteiro aqui.