sql >> Base de Datos >  >> RDS >> Database

T-SQL Tuesday #64:¿Un disparador o muchos?

Es ese martes del mes, ya sabes, en el que ocurre la fiesta de bloque de bloggers conocida como T-SQL Tuesday. Este mes está organizado por Russ Thomas (@SQLJudo) y el tema es "Llamando a todos los sintonizadores y entusiastas". Voy a tratar un problema relacionado con el rendimiento aquí, aunque me disculpo porque podría no estar completamente en línea con las pautas que Russ estableció en su invitación (no voy a usar sugerencias, marcas de rastreo o guías de planes) .

En SQLBits la semana pasada, hice una presentación sobre disparadores, y mi buen amigo y colega MVP Erland Sommarskog asistió. En un momento, sugerí que antes de crear un nuevo disparador en una tabla, debería verificar si ya existen disparadores y considerar combinar la lógica en lugar de agregar un disparador adicional. Mis razones fueron principalmente por la mantenibilidad del código, pero también por el rendimiento. Erland me preguntó si alguna vez había probado para ver si había alguna sobrecarga adicional al tener varios disparadores disparados para la misma acción, y tuve que admitir que no, no había hecho nada extenso. Así que voy a hacer eso ahora.

En AdventureWorks2014, creé un conjunto simple de tablas que básicamente representan sys.all_objects (~2700 filas) y sys.all_columns (~9500 filas). Quería medir el efecto en la carga de trabajo de varios enfoques para actualizar ambas tablas; básicamente, tiene usuarios que actualizan la tabla de columnas y usa un disparador para actualizar una columna diferente en la misma tabla y algunas columnas en la tabla de objetos.

  • T1:línea base :suponga que puede controlar todo el acceso a los datos a través de un procedimiento almacenado; en este caso, las actualizaciones contra ambas tablas se pueden realizar directamente, sin necesidad de disparadores. (Esto no es práctico en el mundo real, porque no puede prohibir de manera confiable el acceso directo a las tablas).
  • T2:disparador único contra otra tabla :suponga que puede controlar la declaración de actualización en la tabla afectada y agregar otras columnas, pero las actualizaciones de la tabla secundaria deben implementarse con un disparador. Actualizaremos las tres columnas con una declaración.
  • T3:disparador único contra ambas tablas :En este caso, tenemos un activador con dos instrucciones, una que actualiza la otra columna de la tabla afectada y otra que actualiza las tres columnas de la tabla secundaria.
  • T4:disparador único contra ambas tablas :Como T3, pero esta vez, tenemos un disparador con cuatro declaraciones, una que actualiza la otra columna en la tabla afectada y una declaración para cada columna actualizada en la tabla secundaria. Esta podría ser la forma en que se maneja si los requisitos se agregan con el tiempo y una declaración separada se considera más segura en términos de prueba de regresión.
  • T5:dos disparadores :Un activador actualiza solo la tabla afectada; el otro usa una sola declaración para actualizar las tres columnas en la tabla secundaria. Esta podría ser la forma en que se hace si los otros factores desencadenantes no se notan o si está prohibido modificarlos.
  • T6:Cuatro disparadores :Un activador actualiza solo la tabla afectada; los otros tres actualizan cada columna en la tabla secundaria. Nuevamente, esta podría ser la forma en que se hace si no sabe que existen otros factores desencadenantes, o si tiene miedo de tocar los otros factores desencadenantes debido a problemas de regresión.

Estos son los datos de origen con los que estamos tratando:

-- 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

Ahora, para cada una de las 6 pruebas, ejecutaremos nuestras actualizaciones 1000 veces y mediremos el tiempo

T1:Línea base

Este es el escenario en el que tenemos la suerte de evitar los factores desencadenantes (de nuevo, no muy realista). En este caso, mediremos las lecturas y la duración de este lote. Pongo /*real*/ en el texto de la consulta para que pueda extraer fácilmente las estadísticas solo para estas declaraciones, y no cualquier declaración dentro de los disparadores, ya que, en última instancia, las métricas se acumulan en las declaraciones que invocan los disparadores. También tenga en cuenta que las actualizaciones reales que estoy haciendo realmente no tienen ningún sentido, así que ignore que estoy configurando la intercalación en el nombre del servidor/instancia y el objeto principal_id al session_id de la sesión actual .

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:disparador único

Para esto necesitamos el siguiente disparador simple, que solo actualiza 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

Entonces nuestro lote solo necesita actualizar las dos columnas en la tabla principal:

UPDATE /*real*/ dbo.tr2 SET name += N'', collation_name = @@SERVERNAME
  WHERE name LIKE '%s%';
GO 1000

T3:disparador único contra ambas tablas

Para esta prueba, nuestro activador se ve así:

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

Y ahora el lote que estamos probando simplemente tiene que actualizar la columna original en la tabla principal; el otro es manejado por el gatillo:

UPDATE /*real*/ dbo.tr3 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T4:disparador único contra ambas tablas

Esto es como T3, pero ahora el activador tiene cuatro declaraciones:

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

El lote de prueba no ha cambiado:

UPDATE /*real*/ dbo.tr4 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T5:Dos disparadores

Aquí tenemos un activador para actualizar la tabla principal y un activador para actualizar la tabla secundaria:

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

El lote de prueba vuelve a ser muy básico:

UPDATE /*real*/ dbo.tr5 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

T6:Cuatro disparadores

Esta vez tenemos un disparador para cada columna que se ve afectada; uno en la tabla principal y tres en las tablas secundarias.

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

Y el lote de prueba:

UPDATE /*real*/ dbo.tr6 SET name += N''
  WHERE name LIKE '%s%';
GO 1000

Medición del impacto de la carga de trabajo

Finalmente, escribí una consulta simple contra sys.dm_exec_query_stats para medir las lecturas y la duración de cada prueba:

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

Realicé las pruebas 10 veces, recopilé los resultados y promedié todo. Así es como se descompuso:

Prueba/Lote Duración media
(microsegundos)
Total de lecturas
(8K páginas)
T1 :ACTUALIZAR /*real*/ dbo.tr1 … 22.608 205.134
T2 :ACTUALIZAR /*real*/ dbo.tr2 … 32.749 11,331,628
T3 :ACTUALIZAR /*real*/ dbo.tr3 … 72.899 22,838,308
T4 :ACTUALIZAR /*real*/ dbo.tr4 … 78.372 44.463.275
T5 :ACTUALIZAR /*real*/ dbo.tr5 … 88.563 41.514.778
T6 :ACTUALIZAR /*real*/ dbo.tr6 … 127.079 100,330,753


Y aquí hay una representación gráfica de la duración:

Conclusión

Está claro que, en este caso, hay una sobrecarga sustancial para cada disparador que se invoca:todos estos lotes finalmente afectaron la misma cantidad de filas, pero en algunos casos, las mismas filas se tocaron varias veces. Probablemente realizaré más pruebas de seguimiento para medir la diferencia cuando la misma fila nunca se toque más de una vez; un esquema más complicado, quizás, en el que se deben tocar otras 5 o 10 tablas cada vez, y estas declaraciones diferentes podrían ser en un solo gatillo o en múltiples. Mi conjetura es que las diferencias de gastos generales se verán más impulsadas por cosas como la simultaneidad y el número de filas afectadas que por los gastos generales del activador en sí, pero ya veremos.

¿Quieres probar la demostración tú mismo? Descarga el guión aquí.