2009-03-16 76 views
20

Nuestro sistema se ejecuta en SQL Server 2000, y estamos en el proceso de preparación para una actualización a SQL Server 2008. Tenemos un montón de código de activación donde tenemos que detectar un cambio en una columna dada y luego operar en esa columna si ha cambiado.Método más eficiente para detectar cambios de columna en MS SQL Server

Obviamente SQL Server proporciona la UPDATE() y COLUMNS_UPDATED() funciones, pero estas funciones sólo le dirá qué columnas se han implicado en la instrucción SQL, no las columnas que han cambiado realmente.

Para determinar las columnas que han cambiado, necesita un código similar a lo siguiente (para una columna que soporta NULL):

IF UPDATE(Col1) 
    SELECT @col1_changed = COUNT(*) 
    FROM Inserted i 
     INNER JOIN Deleted d ON i.Table_ID = d.Table_ID 
    WHERE ISNULL(i.Col1, '<unique null value>') 
      != ISNULL(i.Col1, '<unique null value>') 

Este código debe repetirse para cada columna que está interesado en la prueba. Luego puede verificar el valor "modificado" para determinar si realizar o no operaciones costosas. Por supuesto, este código es en sí mismo problemático, ya que solo le informa que al menos un valor en la columna ha cambiado en todas las filas que se modificaron.

Puede probar UPDATE individuales con algo como esto:

UPDATE Table SET Col1 = CASE WHEN i.Col1 = d.Col1 
      THEN Col1 
      ELSE dbo.fnTransform(Col1) END 
FROM Inserted i 
    INNER JOIN Deleted d ON i.Table_ID = d.Table_ID 

... pero esto no funciona bien cuando estás necesitando para invocar un procedimiento almacenado. En esos casos, tiene que recurrir a otros enfoques por lo que puedo decir.

Mi pregunta es si alguien tiene conocimiento (o, mejor aún, datos duros) sobre cuál es el mejor/más barato enfoque para el problema de predicar una operación de base de datos en un disparador sobre si un valor de columna particular en una fila modificada ha cambiado o no. Ninguno de los métodos anteriores parece ideal, y me preguntaba si existe un mejor método.

+1

He añadido una nueva respuesta a esta vieja pregunta que es relevante: http://stackoverflow.com/questions/1254787/sql-server-update-trigger -get-only-modified-fields/8020461 # 8020461 –

+0

Eso es muy interesante, ¡gracias por el aviso! – mwigdahl

Respuesta

7

Aunque HLGEM dio algunos buenos consejos antes, no era exactamente lo que necesitaba. He realizado algunas pruebas durante los últimos días, y pensé que al menos compartiría los resultados aquí dado que parece que no habrá más información disponible.

Configuré una tabla que efectivamente era un subconjunto más estrecho (9 columnas) de una de las tablas principales de nuestro sistema, y ​​lo llenaba con datos de producción para que fuera tan profundo como nuestra versión de producción de la tabla.

Luego dupliqué esa tabla, y en la primera escribí un disparador que intentaba detectar cada cambio de columna individual, y pronuncié cada actualización de columna sobre si los datos en esa columna realmente habían cambiado o no.

Para la segunda tabla, escribí un disparador que usaba una lógica CASE condicional amplia para hacer todas las actualizaciones de todas las columnas en una sola instrucción.

Entonces me encontré con 4 pruebas:

  1. una actualización de una sola columna a una sola fila
  2. una sola columna de actualización a 10.000 filas
  3. Una actualización de nueve columnas a una sola fila
  4. Una actualización de nueve columnas a 10000 filas

Repetí esta prueba para las versiones indexadas y no indexadas de las tablas, y luego repetí Todo en los servidores SQL 2000 y SQL 2008.

Los resultados que obtuve eran bastante interesante:

El segundo método (una instrucción de actualización individual con lógica CASE peludo en la cláusula SET) fue uniformemente mejor rendimiento que la detección de cambio individual (en un grado mayor o menor dependiendo de la prueba) con la única excepción de un cambio de una sola columna que afecta a muchas filas donde la columna fue indexada, ejecutándose en SQL 2000. En nuestro caso particular, no hacemos muchas actualizaciones estrechas y profundas como esta, entonces para mi propósito el enfoque de declaración única es definitivamente el camino a seguir.


estaría interesado en escuchar los resultados de tipos similares de pruebas, de otras personas para ver si mis conclusiones son tan universales como sospecho que son o si son específicas a nuestra configuración particular.

Para ir abriendo boca, aquí está el script de prueba utilicé - que, obviamente, tendrá que subir con otros datos para rellenarla con:

create table test1 
( 
    t_id int NOT NULL PRIMARY KEY, 
    i1 int NULL, 
    i2 int NULL, 
    i3 int NULL, 
    v1 varchar(500) NULL, 
    v2 varchar(500) NULL, 
    v3 varchar(500) NULL, 
    d1 datetime NULL, 
    d2 datetime NULL, 
    d3 datetime NULL 
) 

create table test2 
( 
    t_id int NOT NULL PRIMARY KEY, 
    i1 int NULL, 
    i2 int NULL, 
    i3 int NULL, 
    v1 varchar(500) NULL, 
    v2 varchar(500) NULL, 
    v3 varchar(500) NULL, 
    d1 datetime NULL, 
    d2 datetime NULL, 
    d3 datetime NULL 
) 

-- optional indexing here, test with it on and off... 
CREATE INDEX [IX_test1_i1] ON [dbo].[test1] ([i1]) 
CREATE INDEX [IX_test1_i2] ON [dbo].[test1] ([i2]) 
CREATE INDEX [IX_test1_i3] ON [dbo].[test1] ([i3]) 
CREATE INDEX [IX_test1_v1] ON [dbo].[test1] ([v1]) 
CREATE INDEX [IX_test1_v2] ON [dbo].[test1] ([v2]) 
CREATE INDEX [IX_test1_v3] ON [dbo].[test1] ([v3]) 
CREATE INDEX [IX_test1_d1] ON [dbo].[test1] ([d1]) 
CREATE INDEX [IX_test1_d2] ON [dbo].[test1] ([d2]) 
CREATE INDEX [IX_test1_d3] ON [dbo].[test1] ([d3]) 

CREATE INDEX [IX_test2_i1] ON [dbo].[test2] ([i1]) 
CREATE INDEX [IX_test2_i2] ON [dbo].[test2] ([i2]) 
CREATE INDEX [IX_test2_i3] ON [dbo].[test2] ([i3]) 
CREATE INDEX [IX_test2_v1] ON [dbo].[test2] ([v1]) 
CREATE INDEX [IX_test2_v2] ON [dbo].[test2] ([v2]) 
CREATE INDEX [IX_test2_v3] ON [dbo].[test2] ([v3]) 
CREATE INDEX [IX_test2_d1] ON [dbo].[test2] ([d1]) 
CREATE INDEX [IX_test2_d2] ON [dbo].[test2] ([d2]) 
CREATE INDEX [IX_test2_d3] ON [dbo].[test2] ([d3]) 

insert into test1 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3) 
-- add data population here... 

insert into test2 (t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3) 
select t_id, i1, i2, i3, v1, v2, v3, d1, d2, d3 from test1 

go 

create trigger test1_update on test1 for update 
as 
begin 

declare @i1_changed int, 
    @i2_changed int, 
    @i3_changed int, 
    @v1_changed int, 
    @v2_changed int, 
    @v3_changed int, 
    @d1_changed int, 
    @d2_changed int, 
    @d3_changed int 

IF UPDATE(i1) 
    SELECT @i1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.i1,0) != ISNULL(d.i1,0) 
IF UPDATE(i2) 
    SELECT @i2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.i2,0) != ISNULL(d.i2,0) 
IF UPDATE(i3) 
    SELECT @i3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.i3,0) != ISNULL(d.i3,0) 
IF UPDATE(v1) 
    SELECT @v1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.v1,'') != ISNULL(d.v1,'') 
IF UPDATE(v2) 
    SELECT @v2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.v2,'') != ISNULL(d.v2,'') 
IF UPDATE(v3) 
    SELECT @v3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.v3,'') != ISNULL(d.v3,'') 
IF UPDATE(d1) 
    SELECT @d1_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.d1,'1/1/1980') != ISNULL(d.d1,'1/1/1980') 
IF UPDATE(d2) 
    SELECT @d2_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.d2,'1/1/1980') != ISNULL(d.d2,'1/1/1980') 
IF UPDATE(d3) 
    SELECT @d3_changed = COUNT(*) FROM Inserted i INNER JOIN Deleted d 
     ON i.t_id = d.t_id WHERE ISNULL(i.d3,'1/1/1980') != ISNULL(d.d3,'1/1/1980') 

if (@i1_changed > 0) 
begin 
    UPDATE test1 SET i1 = CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.i1 != d.i1 
end 

if (@i2_changed > 0) 
begin 
    UPDATE test1 SET i2 = CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.i2 != d.i2 
end 

if (@i3_changed > 0) 
begin 
    UPDATE test1 SET i3 = i.i3^d.i3 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.i3 != d.i3 
end 

if (@v1_changed > 0) 
begin 
    UPDATE test1 SET v1 = i.v1 + 'a' 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.v1 != d.v1 
end 

UPDATE test1 SET v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5) 
FROM test1 
    INNER JOIN inserted i ON test1.t_id = i.t_id 
    INNER JOIN deleted d ON i.t_id = d.t_id 

if (@v3_changed > 0) 
begin 
    UPDATE test1 SET v3 = LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5) 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.v3 != d.v3 
end 

if (@d1_changed > 0) 
begin 
    UPDATE test1 SET d1 = DATEADD(dd, 1, i.d1) 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.d1 != d.d1 
end 

if (@d2_changed > 0) 
begin 
    UPDATE test1 SET d2 = DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2) 
    FROM test1 
     INNER JOIN inserted i ON test1.t_id = i.t_id 
     INNER JOIN deleted d ON i.t_id = d.t_id 
    WHERE i.d2 != d.d2 
end 

UPDATE test1 SET d3 = DATEADD(dd, 15, i.d3) 
FROM test1 
    INNER JOIN inserted i ON test1.t_id = i.t_id 
    INNER JOIN deleted d ON i.t_id = d.t_id 

end 

go 

create trigger test2_update on test2 for update 
as 
begin 

    UPDATE test2 SET 
     i1 = 
      CASE 
      WHEN ISNULL(i.i1, 0) != ISNULL(d.i1, 0) 
      THEN CASE WHEN i.i1 > d.i1 THEN i.i1 ELSE d.i1 END 
      ELSE test2.i1 END, 
     i2 = 
      CASE 
      WHEN ISNULL(i.i2, 0) != ISNULL(d.i2, 0) 
      THEN CASE WHEN i.i2 > d.i2 THEN POWER(i.i2, 1.1) ELSE POWER(d.i2, 1.1) END 
      ELSE test2.i2 END, 
     i3 = 
      CASE 
      WHEN ISNULL(i.i3, 0) != ISNULL(d.i3, 0) 
      THEN i.i3^d.i3 
      ELSE test2.i3 END, 
     v1 = 
      CASE 
      WHEN ISNULL(i.v1, '') != ISNULL(d.v1, '') 
      THEN i.v1 + 'a' 
      ELSE test2.v1 END, 
     v2 = LEFT(i.v2, 5) + '|' + RIGHT(d.v2, 5), 
     v3 = 
      CASE 
      WHEN ISNULL(i.v3, '') != ISNULL(d.v3, '') 
      THEN LEFT(i.v3, 5) + '|' + LEFT(i.v2, 5) + '|' + LEFT(i.v1, 5) 
      ELSE test2.v3 END, 
     d1 = 
      CASE 
      WHEN ISNULL(i.d1, '1/1/1980') != ISNULL(d.d1, '1/1/1980') 
      THEN DATEADD(dd, 1, i.d1) 
      ELSE test2.d1 END, 
     d2 = 
      CASE 
      WHEN ISNULL(i.d2, '1/1/1980') != ISNULL(d.d2, '1/1/1980') 
      THEN DATEADD(dd, DATEDIFF(dd, i.d2, d.d2), d.d2) 
      ELSE test2.d2 END, 
     d3 = DATEADD(dd, 15, i.d3) 
    FROM test2 
     INNER JOIN inserted i ON test2.t_id = i.t_id 
     INNER JOIN deleted d ON test2.t_id = d.t_id 

end 

go 

----- 
-- the below code can be used to confirm that the triggers operated identically over both tables after a test 
select top 10 test1.i1, test2.i1, test1.i2, test2.i2, test1.i3, test2.i3, test1.v1, test2.v1, test1.v2, test2.v2, test1.v3, test2.v3, test1.d1, test1.d1, test1.d2, test2.d2, test1.d3, test2.d3 
from test1 inner join test2 on test1.t_id = test2.t_id 
where 
    test1.i1 != test2.i1 or 
    test1.i2 != test2.i2 or 
    test1.i3 != test2.i3 or 
    test1.v1 != test2.v1 or 
    test1.v2 != test2.v2 or 
    test1.v3 != test2.v3 or 
    test1.d1 != test2.d1 or 
    test1.d2 != test2.d2 or 
    test1.d3 != test2.d3 

-- test 1 -- one column, one row 
update test1 set i3 = 64 where t_id = 1000 
go 
update test2 set i3 = 64 where t_id = 1000 
go 

update test1 set i3 = 64 where t_id = 1001 
go 
update test2 set i3 = 64 where t_id = 1001 
go 

-- test 2 -- one column, 10000 rows 
update test1 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000 
go 
update test2 set v3 = LEFT(v3, 50) where t_id between 10000 and 20000 
go 

-- test 3 -- all columns, 1 row, non-self-referential 
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id = 3000 
go 
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id = 3000 
go 

-- test 4 -- all columns, 10000 rows, non-self-referential 
update test1 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id between 30000 and 40000 
go 
update test2 set i1 = 1000, i2 = 2000, i3 = 3000, v1 = 'R12345123', v2 = 'Happy!', v3 = 'I am v3!!!', d1 = '1/1/1985', d2 = '1/1/1988', d3 = NULL 
where t_id between 30000 and 40000 
go 

----- 

drop table test1 
drop table test2 
16

Comencemos con Nunca lo haría y quiero decir nunca invocar un proceso almacenado en un desencadenador. Para tener en cuenta una inserción de varias filas, debe desplazar el cursor por el proceso. Esto significa que las 200,000 filas que acaba de cargar a través de una consulta basada en conjuntos (por ejemplo, actualizar todos los precios en un 10%) bien podrían bloquear la tabla durante horas, ya que el desencadenador intenta valientemente manejar la carga. Además, si algo cambia en el proceso, podrías romper cualquier inserción en la mesa o incluso colgar completamente la mesa. Soy un firme creyente de que el código de activación no debería llamar a nada más fuera del gatillo.

Personalmente prefiero simplemente hacer mi tarea. Si escribí las acciones que deseo hacer correctamente en el desencadenador, solo se actualizarán, eliminarán o insertarán las columnas que hayan cambiado.

Ejemplo: supongamos que desea actualizar el último campo de nombre que está almacenando en dos lugares debido a una desnormalización colocada allí por motivos de rendimiento.

update t 
set lname = i.lname 
from table2 t 
join inserted i on t.fkfield = i.pkfield 
where t.lname <>i.lname 

Como se puede ver que sólo actualizar los lnames que son diferentes de lo que es actualmente en la tabla Estoy actualizando.

Si usted quiere hacer la auditoría y grabar sólo las filas que cambiaron luego hacer la comparación utilizando todos los campos algo así como donde i.field1 <> d.field1 o i.field2 <> d.field3 (etc a través de toda la campos)

+0

En el caso que presente, terminaría bloqueando table2 para la actualización de cada modificación que realice en la tabla original, incluso si nunca modificó lname en absoluto. Eso es parte de lo que trato de evitar. ¡Gracias por el consejo, sin embargo! – mwigdahl

+1

He votado esto porque me enteré de la manera difícil de no llamar a los SP de los desencadenantes ... ¡Nunca más! – RolandTumble

9

creo que es posible que desee investigar usando el operador Excepto. Es un operador basado en conjuntos que puede eliminar las filas que no han cambiado. Lo bueno es que tiene en cuenta los valores nulos tan iguales como se ve por las filas de la primera serie enumerados antes de la EXCEPT y no en la segunda lista después de la SALVO

WITH ChangedData AS (
SELECT d.Table_ID , d.Col1 FROM deleted d 
EXCEPT 
SELECT i.Table_ID , i.Col1 FROM inserted i 
) 
/*Do Something with the ChangedData */ 

Este maneja el tema de columnas que permiten valores nulos y sin el uso de ISNULL() en el desencadenador y solo devuelve los identificadores de las filas con cambios en col1 para un enfoque basado en un conjunto agradable para detectar cambios. No he probado el enfoque, pero bien puede valer la pena. Creo que EXCEPT fue presentado con SQL Server 2005.

+0

Voy a investigar esto, gracias! – mwigdahl

+0

Estoy usando este método bastante en mi base de datos y aunque no he medido el rendimiento, parece bastante rápido. No he medido porque no veo un golpe de rendimiento. Por cierto, mi lógica pone el INSERT antes de EXCEPT y maneja la detección de INSERTAR y ACTUALIZAR los cambios. Por cierto, no he usado la declaración "CON", pero parece interesante. Ver mi respuesta ligeramente diferente a continuación. –

5

Recomiendo usar el operador EXCEPT set mencionado por Todd/arghtype arriba.

He agregado esta respuesta porque puse el "insertado" antes de la "eliminada" para que se detecten los INSERT y las ACTUALIZACIONES. Así que generalmente puedo tener un disparador para cubrir tanto insertos como actualizaciones. También puede detectar eliminaciones al agregar OR (NO EXISTE (SELECCIONAR * FROM insertada) Y EXISTE (SELECCIONAR * FROM eliminado))

Determina si un valor ha cambiado solo en las columnas especificadas. No he investigado su rendimiento en comparación con las otras soluciones, pero está funcionando bien en mi base de datos.

Utiliza el operador de conjunto EXCEPT para devolver las filas de la consulta de la izquierda que no se encuentran también en la consulta correcta. Este código se puede usar en INSERT, UPDATE y DELETE triggers.

La columna "PKID" es la clave principal. Se requiere para habilitar la coincidencia entre los dos conjuntos. Si tiene varias columnas para la clave principal, deberá incluir todas las columnas para hacer una coincidencia correcta entre los conjuntos insertados y eliminados.

-- Only do trigger logic if specific field values change. 
IF EXISTS(SELECT PKID 
       ,Column1 
       ,Column7 
       ,Column10 
      FROM inserted 
      EXCEPT 
      SELECT PKID 
       ,Column1 
       ,Column7 
       ,Column10 
      FROM deleted) -- Tests for modifications to fields that we are interested in 
OR (NOT EXISTS(SELECT * FROM inserted) AND EXISTS(SELECT * FROM deleted)) -- Have a deletion 
BEGIN 
      -- Put code here that does the work in the trigger 

END 

Si desea utilizar las filas modificados en la lógica de disparo posterior, yo suelo poner los resultados de la consultas excepto en una variable de tabla que se puede hacer referencia más adelante.

espero que esto es de interés :-)

Cuestiones relacionadas