web-dev-qa-db-fra.com

Méthode la plus efficace pour détecter le changement de colonne dans MS SQL Server

Notre système fonctionne sur SQL Server 2000, et nous sommes en train de préparer une mise à niveau vers SQL Server 2008. Nous avons beaucoup de code de déclenchement où nous devons détecter un changement dans une colonne donnée et ensuite opérer sur cette colonne si elle a changé.

De toute évidence, SQL Server fournit les fonctions PDATE () et COLUMNS_UPDATED () , mais ces fonctions vous indiquent uniquement quelles colonnes ont été impliquées dans l'instruction SQL, pas quelles colonnes ont réellement changé.

Pour déterminer quelles colonnes ont changé, vous avez besoin d'un code similaire au suivant (pour une colonne qui prend en charge les valeurs 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>')

Ce code doit être répété pour chaque colonne que vous souhaitez tester. Vous pouvez ensuite vérifier la valeur "modifiée" pour déterminer s'il faut effectuer ou non des opérations coûteuses. Bien sûr, ce code est lui-même problématique, car il vous indique seulement qu'au moins une valeur de la colonne a changé sur toutes les lignes qui ont été modifiées.

Vous pouvez tester des instructions UPDATE individuelles avec quelque chose comme ceci:

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

... mais cela ne fonctionne pas bien lorsque vous devez appeler une procédure stockée. Dans ces cas, vous devez vous rabattre sur d'autres approches pour autant que je sache.

Ma question est de savoir si quelqu'un a une idée (ou, mieux encore, des données matérielles) de l'approche la meilleure/la moins chère au problème de prédire une opération de base de données dans un déclencheur pour savoir si une valeur de colonne particulière dans une ligne modifiée a réellement changé ou ne pas. Aucune des méthodes ci-dessus ne semble idéale, et je me demandais s'il existait une meilleure méthode.

25
mwigdahl

Bien que HLGEM ait donné de bons conseils ci-dessus, ce n'était pas exactement ce dont j'avais besoin. J'ai fait pas mal de tests au cours des derniers jours, et je me suis dit que je partagerais au moins les résultats ici étant donné qu'il ne semble pas y avoir d'informations supplémentaires.

J'ai mis en place une table qui était en fait un sous-ensemble plus étroit (9 colonnes) de l'une des tables principales de notre système, et je l'ai remplie avec des données de production afin qu'elle soit aussi profonde que notre version de production de la table.

J'ai ensuite dupliqué cette table, et sur la première, j'ai écrit un déclencheur qui a tenté de détecter chaque changement de colonne individuel, puis j'ai prédit chaque mise à jour de colonne pour savoir si les données de cette colonne avaient réellement changé ou non.

Pour le deuxième tableau, j'ai écrit un déclencheur qui utilise une logique CASE conditionnelle étendue pour effectuer toutes les mises à jour de toutes les colonnes dans une seule instruction.

J'ai ensuite effectué 4 tests:

  1. Une mise à jour d'une colonne vers une seule ligne
  2. Une mise à jour sur une seule colonne à 10000 lignes
  3. Une mise à jour de neuf colonnes sur une seule ligne
  4. Une mise à jour de neuf colonnes à 10000 lignes

J'ai répété ce test pour les versions indexées et non indexées des tables, puis j'ai répété le tout sur les serveurs SQL 2000 et SQL 2008.

Les résultats que j'ai obtenus étaient assez intéressants:

La deuxième méthode (une seule déclaration de mise à jour avec une logique CASE velue dans la clause SET) était uniformément plus performante que la détection de changement individuel (dans une mesure plus ou moins grande selon le test), à la seule exception d'un changement sur une seule colonne affectant de nombreuses lignes où la colonne a été indexée, s'exécutant sur SQL 2000. Dans notre cas particulier, nous ne faisons pas beaucoup de mises à jour étroites et approfondies comme celle-ci, donc pour moi, l'approche à instruction unique est certainement la voie à suivre.


Je serais intéressé à entendre les résultats d'autres personnes sur des types de tests similaires, pour voir si mes conclusions sont aussi universelles que je le pense ou si elles sont spécifiques à notre configuration particulière.

Pour commencer, voici le script de test que j'ai utilisé - vous aurez évidemment besoin de trouver d'autres données pour le remplir:

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

Commençons par je ne ferais jamais et je veux dire ne jamais invoquer un proc stocké dans un déclencheur. Pour prendre en compte un insert multi-lignes, vous devrez faire défiler le proc. Cela signifie que les 200 000 lignes que vous venez de charger, bien qu'une requête basée sur un ensemble (par exemple, la mise à jour de tous les prix de 10%) pourrait bien verrouiller la table pendant des heures alors que le déclencheur essaie vaillamment de gérer la charge. De plus, si quelque chose change dans le processus, vous pouvez casser des insertions sur la table ou même raccrocher complètement la table. Je suis un fervent partisan que le code de déclenchement ne devrait appeler rien d'autre en dehors du déclencheur.

Personnellement, je préfère simplement faire ma tâche. Si j'ai écrit les actions que je veux faire correctement dans le déclencheur, il ne mettra à jour, supprimera ou insérera que les colonnes qui ont changé.

Exemple: supposons que vous souhaitiez mettre à jour le champ last_name que vous stockez à deux endroits en raison d'une dénormalisation placée pour des raisons de performances.

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

Comme vous pouvez le voir, cela ne mettrait à jour que les noms qui sont différents de ceux qui figurent actuellement dans le tableau que je mets à jour.

Si vous voulez faire un audit et enregistrer uniquement les lignes qui ont changé, faites la comparaison en utilisant tous les champs quelque chose comme où i.field1 <> d.field1 ou i.field2 <> d.field3 (etc. à travers tous les champs)

19
HLGEM

Je pense que vous voudrez peut-être enquêter en utilisant l'opérateur EXCEPT. Il s'agit d'un opérateur basé sur un ensemble qui peut éliminer les lignes qui n'ont pas changé. La bonne chose est que les valeurs nulles sont considérées comme égales car elles recherchent des lignes dans le premier ensemble répertorié avant l'opérateur EXCEPT et non dans le second répertorié après l'EXCEPT

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

Cela gère le problème des colonnes qui autorisent Nulls sans utiliser ISNULL() dans le déclencheur et ne renvoie que les identifiants des lignes avec les modifications apportées à col1 pour une approche basée sur un bel ensemble pour détecter les modifications. Je n'ai pas testé l'approche mais cela vaut peut-être la peine. Je pense qu'EXCEPT a été introduit avec SQL Server 2005.

10
Todd

Je recommande d'utiliser l'opérateur EXCEPT set comme mentionné par Todd/arghtype ci-dessus.

J'ai ajouté cette réponse parce que j'ai mis "inséré" avant "supprimé" afin que les INSERT soient détectés ainsi que les MISES À JOUR. Je peux donc généralement avoir un déclencheur pour couvrir les insertions et les mises à jour. Peut également détecter les suppressions en ajoutant OR (PAS EXISTE (SELECT * FROM inséré) ET EXISTE (SELECT * FROM supprimé))

Il détermine si une valeur a changé uniquement dans les colonnes spécifiées. Je n'ai pas étudié ses performances par rapport aux autres solutions mais cela fonctionne bien dans ma base de données.

Il utilise l'opérateur EXCEPT set pour renvoyer toutes les lignes de la requête de gauche qui ne se trouvent pas également dans la requête de droite. Ce code peut être utilisé dans les déclencheurs INSERT, UPDATE et DELETE.

La colonne "PKID" est la clé primaire. Il est nécessaire d'activer la correspondance entre les deux ensembles. Si vous avez plusieurs colonnes pour la clé primaire, vous devrez inclure toutes les colonnes pour faire une correspondance correcte entre les ensembles insérés et supprimés.

-- 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 vous souhaitez utiliser les lignes modifiées dans la logique de déclenchement suivante, je place généralement les résultats de la requête EXCEPT dans une variable de table qui peut être référencée ultérieurement.

J'espère que cela vous intéresse :-)

6
David Coster

Il existe une autre technique dans SQL Server 2008 pour le suivi des modifications:

Comparaison de la capture de données modifiées et du suivi des modifications

4
Serg