Performances atroces reliant les tables INSÉRÉES et SUPPRIMÉES dans un déclencheur

12

J'ai un déclencheur UPDATE sur une table qui surveille une colonne spécifique passant d'une valeur spécifique à une autre valeur. Lorsque cela se produit, il met à jour certaines données associées dans une autre table via une seule instruction UPDATE.

La première chose que le déclencheur fait est de vérifier si des lignes mises à jour ont changé la valeur de cette colonne par rapport à la valeur en question. Il joint simplement INSERTED à DELETED et compare la valeur de cette colonne. Si rien ne se qualifie, il sort tôt pour que l'instruction UPDATE ne s'exécute pas.

IF NOT EXISTS (
    SELECT TOP 1 i.CUSTNMBR
    FROM INSERTED i
        INNER JOIN DELETED d
            ON i.CUSTNMBR = d.CUSTNMBR
    WHERE d.CUSTCLAS = 'Misc'
        AND i.CUSTCLAS != 'Misc'
)
    RETURN

Dans ce cas, CUSTNMBR est la clé primaire de la table sous-jacente. Si je fais une grande mise à jour sur cette table (disons, plus de 5000 lignes), cette instruction prend AGES, même si je n'ai pas touché la colonne CUSTCLAS. Je peux le regarder caler sur cette déclaration pendant plusieurs minutes dans Profiler.

Le plan d'exécution est bizarre. Il montre un scan inséré avec 3 714 exécutions et ~ 18,5 millions de lignes de sortie. Cela passe par un filtre sur la colonne CUSTCLAS. Il joint cela (via une boucle imbriquée) à un balayage supprimé (également filtré sur CUSTCLAS), qui ne s'exécute qu'une seule fois et possède 5000 lignes de sortie.

Quelle chose idiote fais-je ici pour provoquer cela? Notez que le déclencheur doit absolument gérer correctement les mises à jour multi-lignes.

MODIFIER :

J'ai aussi essayé de l'écrire comme ça (au cas où EXISTS faisait quelque chose de désagréable), mais c'est toujours aussi terrible.

DECLARE @CUSTNMBR varchar(31)
SELECT TOP 1 @CUSTNMBR = i.CUSTNMBR
FROM INSERTED i
    INNER JOIN DELETED d
        ON i.CUSTNMBR = d.CUSTNMBR
WHERE d.CUSTCLAS = 'Misc'
    AND i.CUSTCLAS != 'Misc'

IF @CUSTNMBR IS NULL
    RETURN
db2
la source
Pouvez-vous vous débarrasser du "TOP 1"? Je pense que cela cause des frais généraux qui ne seront peut-être pas nécessaires si vous vérifiez simplement s'il y a un seul cas ...
JHFB

Réponses:

10

Vous pouvez évaluer en utilisant des explicites INNER MERGE JOINou des INNER HASH JOINconseils, mais étant donné que vous utilisez probablement ces tables à nouveau plus tard dans le déclencheur, vous feriez probablement mieux d'insérer simplement le contenu insertedet les deletedtables dans des #temptables indexées et d'en finir avec.

Ils n'obtiennent pas automatiquement des index utiles créés pour eux.

Martin Smith
la source
D'accord, cela accélère énormément, mais il existe un potentiel d'exécution en cascade du déclencheur. Si j'utilise les mêmes noms de table temporaire (#i, #d) dans chaque déclencheur, ils sont en conflit. Existe-t-il une solution meilleure / plus sûre que l'utilisation d'un nom de table temporaire différent dans chaque déclencheur?
db2
Pourrait évaluer à l'aide de variables de table (avec une clé primaire définie sur CUSTNMBRpour créer l'index clusterisé unique) et utiliser l' OPTION (RECOMPILE)indice pour qu'il prenne en compte le nombre de lignes ou peut-être simplement utiliser une convention de dénomination particulière telle que#i_dbo_YourTable
Martin Smith
Je pense que je vais me contenter de les nommer comme #trigger_name_i. Si je choisis des variables de table, je devrai encore plus encombrer le code avec des CREATE TABLE explicites. Nous avons des déclencheurs en cascade, mais pas des déclencheurs récursifs, donc je pense que je serai en sécurité ...
db2
Je recommande une variable de table au lieu d'une table temporaire à cet effet; les variables de table peuvent toujours avoir des index primaires et secondaires (uniques), elles sont automatiquement nettoyées lorsque le déclencheur se termine et les variables de table sont limitées à cette exécution de déclencheur (elle n'entrera pas en conflit avec d'autres variables de table du même nom supérieures ou inférieures sur la pile des appels). Pour économiser sur la surcharge du code de définition de table, définissez un type de table pour chacun et utilisez le nom du type pour déclarer les variables de table.
Chris Smith
@ChrisSmith dont vous auriez souvent besoin OPTION (RECOMPILE), la cardinalité est donc prise en compte.
Martin Smith
10

Je sais que cela a été répondu, mais il est apparu comme récemment actif et je l'ai rencontré aussi pour des tables avec plusieurs millions de lignes. Bien que je n'écarte pas la réponse acceptée, je peux au moins ajouter que mon expérience montre qu'un facteur clé des performances de déclenchement lors de tests similaires (voir si une ou plusieurs colonnes ont réellement changé leurs valeurs) est de savoir si la ou les colonnes en cours de test faisaient en fait partie de la UPDATEdéclaration. J'ai trouvé que la comparaison des colonnes entre les tables insertedet deletedqui ne faisaient en fait pas partie de l' UPDATEinstruction affectait considérablement les performances qui n'existaient pas autrement si ces champs faisaient partie de laUPDATE(quelle que soit leur valeur réellement modifiée). Pourquoi tout cela fonctionne (c'est-à-dire une requête pour comparer N champs sur X lignes) pour déterminer si quelque chose a changé si vous pouvez exclure logiquement la possibilité de modifier l'une de ces colonnes, ce qui n'est évidemment pas possible si elles n'étaient pas présentes dans la SETclause de la UPDATEdéclaration.

La solution que j'ai employée était d'utiliser la fonction UPDATE () qui ne fonctionne qu'à l'intérieur des déclencheurs. Cette fonction intégrée vous indique si une colonne a été spécifiée dans l' UPDATEinstruction et peut être utilisée pour quitter le déclencheur si les colonnes qui vous intéressent ne font pas partie de la UPDATE. Cela peut être utilisé conjointement avec a SELECTpour déterminer si ces colonnes, en supposant qu'elles sont présentes dans le UPDATE, ont des changements réels. J'ai du code en haut de plusieurs déclencheurs d'audit qui ressemble à:

-- exit on updates that do not update the only 3 columns we ETL
IF (
     EXISTS(SELECT 1 FROM DELETED) -- this is an UPDATE (Trigger is AFTER INSERT, UPDATE)
     AND (
            NOT (UPDATE(Column3) OR UPDATE(Column7)
                 OR UPDATE(Column11)) -- the columns we care about are not being updated
            OR NOT EXISTS(
                        SELECT 1
                        FROM INSERTED ins
                        INNER JOIN DELETED del
                                ON del.KeyField1 = ins.KeyField1
                                AND del.KeyField2 = ins.KeyField2
                        WHERE ins.Column3 <> del.Column3
                                 COLLATE Latin1_General_100_CS_AS -- case-sensitive compare
                        OR    ISNULL(ins.Column7, -99) <> 
                                 ISNULL(del.Column7, -99) -- NULLable INT field
                        OR    ins.[Column11] <> del.[Column11] -- NOT NULL INT field
                      )
          )
    )
BEGIN
    RETURN;
END;

Cette logique passera au reste du déclencheur si:

  1. L'opération est un INSERT
  2. Au moins un des champs pertinents se trouve dans la SETclause d'un UPDATE et au moins une de ces colonnes dans une ligne a changé

Le NOT (UPDATE...) OR NOT EXISTS()peut sembler étrange ou en arrière, mais il est conçu pour éviter de faire le SELECTsur les tables insertedet deletedsi aucune des colonnes pertinentes ne fait partie de la UPDATE.

Selon vos besoins, la fonction COLUMNS_UPDATED () est une autre option pour déterminer quelles colonnes font partie de l' UPDATEinstruction.

Solomon Rutzky
la source
1
Bon point qu'ils devraient vérifier UPDATE(CUSTCLAS)et simplement sauter le tout si faux (+1). Je ne pense pas que vous ayez raison de dire que les colonnes non mises à jour ne sont pas aussi facilement disponibles dans les versions de lignes que celles mises à jour.
Martin Smith
@MartinSmith, comment allons-nous le prouver dans un sens ou dans l'autre? Cependant, cela pourrait ne pas avoir d'importance si le comportement est prévisible de la manière que j'ai trouvée. Je sais juste que c'est une différence de performance drastique en faisant le même SELECT, JOINing entre INSERTED et DELETED, en vérifiant les champs pour les différences réelles, selon que les champs dans le WHERE étaient dans le SET de la UPDATE ou non. Le comportement que j'ai vu est cohérent, d'où ma théorie, mais il serait bon / intéressant de connaître la vraie raison. Je soupçonnais que les champs qui n'étaient pas dans le SET devaient retourner à la table de base pour leur valeur.
Solomon Rutzky
J'ai déjà examiné la structure de cela. Je ne me souviens pas si j'ai trouvé un bon moyen de le faire ou si je viens d'utiliser une chaîne facilement trouvable et une recherche exhaustive tempdbavecDBCC PAGE
Martin Smith
D'ACCORD. Sur une instance avec un fichier unique de taille minimale, tempdbj'ai juste essayé ce script , collé la sortie dans le bloc-notes et recherché "EEEEEE". Je vois la sortie dans la capture d'écran ici . Notez les versions avant et après des deux colonnes dans les deux lignes. Il peut y avoir des moyens beaucoup plus faciles mais suffisants pour mes besoins ici!
Martin Smith
Bien qu'en réalité, il y ait d'autres longues chaînes EEEEEE dans les tempdbpages non à côté de BBBBBBou DDDDDD. Il faudra peut-être enquêter un peu plus! Mais c'est peut-être dû à l' REPLICATEappel.
Martin Smith
2

Je pourrais essayer de réécrire en utilisant s'il existe

IF EXISTS (SELECT TOP 1 i.CUSTNMBR     
            FROM INSERTED i         
            INNER JOIN DELETED d             
            ON i.CUSTNMBR = d.CUSTNMBR and d.custclass = 'Misc'  
            WHERE d.CUSTCLAS <>i.CUSTCLAS)    
BEGIN

--do your triggerstuff here
END
HLGEM
la source
1

http://dave.brittens.org/blog/writing-well-behaved-triggers.html

Selon Dave, vous devez utiliser des tables temporaires ou des variables de table avec des index, car les tables virtuelles INSERTED / DELETED n'en ont pas. Si vous avez la possibilité de déclencheurs récursifs, vous devez utiliser des variables de table pour éviter les collisions de noms.

J'espère que quelqu'un trouvera cela utile car le message d'origine était il y a un certain temps ...

Keith
la source
-1

Le code suivant peut augmenter les performances de ce déclencheur. Je ne connaissais pas le type de données correct de la colonne [custclass] , vous devez donc l'ajuster.

DECLARE @i AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
DECLARE @d AS TABLE (CUSTNMBR VARCHAR(31) NOT NULL PRIMARY KEY, custclass VARCHAR(10) NOT NULL)
INSERT INTO @i SELECT CUSTNMBR, custclass FROM inserted
INSERT INTO @d SELECT CUSTNMBR, custclass FROM deleted
IF NOT EXISTS
  (SELECT * FROM @i AS i INNER JOIN @d AS d ON d.CUSTNMBR = i.CUSTNMBR
   WHERE i.custclass <> d.custclass) RETURN

Notez que vous pouvez inclure des colonnes supplémentaires dans ces copies en mémoire des tables insérées et supprimées si vous en avez besoin dans votre code de déclenchement. Les clés primaires de ces tables augmenteront considérablement les performances de jointure lors de la mise à jour de plusieurs lignes à la fois. Bonne chance!

Dony
la source