Suppression lente des enregistrements lorsqu'un déclencheur est activé

17

Je pensais que cela avait été résolu avec le lien ci-dessous - le travail autour fonctionne - mais pas le patch. Travailler avec le support Microsoft pour résoudre.

http://support.microsoft.com/kb/2606883

Ok, j'ai donc un problème que je voulais signaler à StackOverflow pour voir si quelqu'un a une idée.

Notez que c'est avec SQL Server 2008 R2

Problème: la suppression de 3000 enregistrements d'une table avec 15000 enregistrements prend 3-4 minutes lorsqu'un déclencheur est activé et seulement 3-5 secondes lorsque le déclencheur est désactivé.

Configuration de la table

Nous nommerons deux tables principales et secondaires. Secondaire contient les enregistrements des éléments que je veux supprimer, donc lorsque j'effectue la suppression, je me joins à la table secondaire. Un processus s'exécute avant l'instruction delete pour remplir la table secondaire avec les enregistrements à supprimer.

Supprimer la déclaration:

DELETE FROM MAIN 
WHERE ID IN (
   SELECT Secondary.ValueInt1 
   FROM Secondary 
   WHERE SECONDARY.GUID = '9FFD2C8DD3864EA7B78DA22B2ED572D7'
);

Ce tableau comporte de nombreuses colonnes et environ 14 index NC différents. J'ai essayé un tas de choses différentes avant de déterminer que le déclencheur était le problème.

  • Activer le verrouillage de page (nous avons désactivé par défaut)
  • Statistiques collectées manuellement
  • Collecte automatique des statistiques désactivée
  • Indice de santé et fragmentation vérifiés
  • Suppression de l'index cluster de la table
  • Examiné le plan d'exécution (rien n'apparaissant comme des index manquants et le coût était de 70% vers la suppression réelle avec environ 28% pour la jointure / fusion des enregistrements

Déclencheurs

Le tableau a 3 déclencheurs (un pour les opérations d'insertion, de mise à jour et de suppression). J'ai modifié le code pour que le déclencheur de suppression revienne, puis en sélectionne un pour voir combien de fois il est déclenché. Il ne se déclenche qu'une seule fois pendant toute l'opération (comme prévu).

ALTER TRIGGER [dbo].[TR_MAIN_RD] ON [dbo].[MAIN]
            AFTER DELETE
            AS  
                SELECT 1
                RETURN

Récapituler

  • Avec Trigger activé - la déclaration prend 3 à 4 minutes pour terminer
  • Avec Trigger désactivé - la déclaration prend 3-5 secondes pour terminer

Quelqu'un a une idée de pourquoi?

Notez également - ne cherchez pas à changer cette architecture, ajoutez des index de suppression, etc. comme solution. Cette table est la pièce maîtresse de certaines opérations de données majeures et nous avons dû la modifier et la régler (index, verrouillage de page, etc.) pour permettre aux principales opérations de concurrence de fonctionner sans blocages.

Voici le plan d'exécution xml (les noms ont été modifiés pour protéger les innocents)

<?xml version="1.0" encoding="utf-16"?>
<ShowPlanXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Version="1.1" Build="10.50.1790.0" xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan">
  <BatchSequence>
    <Batch>
      <Statements>
        <StmtSimple StatementCompId="1" StatementEstRows="185.624" StatementId="1" StatementOptmLevel="FULL" StatementOptmEarlyAbortReason="GoodEnoughPlanFound" StatementSubTreeCost="0.42706" StatementText="DELETE FROM MAIN WHERE ID IN (SELECT Secondary.ValueInt1 FROM Secondary WHERE Secondary.SetTMGUID = '9DDD2C8DD3864EA7B78DA22B2ED572D7')" StatementType="DELETE" QueryHash="0xAEA68D887C4092A1" QueryPlanHash="0x78164F2EEF16B857">
          <StatementSetOptions ANSI_NULLS="true" ANSI_PADDING="true" ANSI_WARNINGS="true" ARITHABORT="false" CONCAT_NULL_YIELDS_NULL="true" NUMERIC_ROUNDABORT="false" QUOTED_IDENTIFIER="true" />
          <QueryPlan CachedPlanSize="48" CompileTime="20" CompileCPU="20" CompileMemory="520">
            <RelOp AvgRowSize="9" EstimateCPU="0.00259874" EstimateIO="0.296614" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="185.624" LogicalOp="Delete" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Delete" EstimatedTotalSubtreeCost="0.42706">
              <OutputList />
              <Update WithUnorderedPrefetch="true" DMLRequestSort="false">
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_02]" IndexKind="Clustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[PK_MAIN_ID]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[UK_MAIN_01]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_03]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_04]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_05]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_06]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_07]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_08]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_09]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_10]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_11]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[UK_MAIN_12]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_13]" IndexKind="NonClustered" />
                <RelOp AvgRowSize="15" EstimateCPU="1.85624E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="185.624" LogicalOp="Top" NodeId="2" Parallel="false" PhysicalOp="Top" EstimatedTotalSubtreeCost="0.127848">
                  <OutputList>
                    <ColumnReference Column="Uniq1002" />
                    <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                  </OutputList>
                  <Top RowCount="true" IsPercent="false" WithTies="false">
                    <TopExpression>
                      <ScalarOperator ScalarString="(0)">
                        <Const ConstValue="(0)" />
                      </ScalarOperator>
                    </TopExpression>
                    <RelOp AvgRowSize="15" EstimateCPU="0.0458347" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="185.624" LogicalOp="Left Semi Join" NodeId="3" Parallel="false" PhysicalOp="Merge Join" EstimatedTotalSubtreeCost="0.12783">
                      <OutputList>
                        <ColumnReference Column="Uniq1002" />
                        <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                      </OutputList>
                      <Merge ManyToMany="false">
                        <InnerSideJoinColumns>
                          <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                        </InnerSideJoinColumns>
                        <OuterSideJoinColumns>
                          <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                        </OuterSideJoinColumns>
                        <Residual>
                          <ScalarOperator ScalarString="[MyDatabase].[dbo].[MAIN].[ID]=[MyDatabase].[dbo].[Secondary].[ValueInt1]">
                            <Compare CompareOp="EQ">
                              <ScalarOperator>
                                <Identifier>
                                  <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                                </Identifier>
                              </ScalarOperator>
                              <ScalarOperator>
                                <Identifier>
                                  <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                                </Identifier>
                              </ScalarOperator>
                            </Compare>
                          </ScalarOperator>
                        </Residual>
                        <RelOp AvgRowSize="19" EstimateCPU="0.0174567" EstimateIO="0.0305324" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="15727" LogicalOp="Index Scan" NodeId="4" Parallel="false" PhysicalOp="Index Scan" EstimatedTotalSubtreeCost="0.0479891" TableCardinality="15727">
                          <OutputList>
                            <ColumnReference Column="Uniq1002" />
                            <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                            <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                          </OutputList>
                          <IndexScan Ordered="true" ScanDirection="FORWARD" ForcedIndex="false" ForceSeek="false" NoExpandHint="false">
                            <DefinedValues>
                              <DefinedValue>
                                <ColumnReference Column="Uniq1002" />
                              </DefinedValue>
                              <DefinedValue>
                                <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                              </DefinedValue>
                              <DefinedValue>
                                <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                              </DefinedValue>
                            </DefinedValues>
                            <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[PK_MAIN_ID]" IndexKind="NonClustered" />
                          </IndexScan>
                        </RelOp>
                        <RelOp AvgRowSize="11" EstimateCPU="0.00392288" EstimateIO="0.03008" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="3423.53" LogicalOp="Index Seek" NodeId="5" Parallel="false" PhysicalOp="Index Seek" EstimatedTotalSubtreeCost="0.0340029" TableCardinality="171775">
                          <OutputList>
                            <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                          </OutputList>
                          <IndexScan Ordered="true" ScanDirection="FORWARD" ForcedIndex="false" ForceSeek="false" NoExpandHint="false">
                            <DefinedValues>
                              <DefinedValue>
                                <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                              </DefinedValue>
                            </DefinedValues>
                            <Object Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Index="[IX_Secondary_01]" IndexKind="NonClustered" />
                            <SeekPredicates>
                              <SeekPredicateNew>
                                <SeekKeys>
                                  <Prefix ScanType="EQ">
                                    <RangeColumns>
                                      <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="SetTMGUID" />
                                    </RangeColumns>
                                    <RangeExpressions>
                                      <ScalarOperator ScalarString="'9DDD2C8DD3864EA7B78DA22B2ED572D7'">
                                        <Const ConstValue="'9DDD2C8DD3864EA7B78DA22B2ED572D7'" />
                                      </ScalarOperator>
                                    </RangeExpressions>
                                  </Prefix>
                                </SeekKeys>
                              </SeekPredicateNew>
                            </SeekPredicates>
                          </IndexScan>
                        </RelOp>
                      </Merge>
                    </RelOp>
                  </Top>
                </RelOp>
              </Update>
            </RelOp>
          </QueryPlan>
        </StmtSimple>
      </Statements>
    </Batch>
  </BatchSequence>
</ShowPlanXML>
tsells
la source

Réponses:

12

L'infrastructure de versionnage de lignes introduite dans SQL Server 2005 est utilisée pour prendre en charge un certain nombre de fonctionnalités, notamment les nouveaux niveaux d'isolement des transactions READ_COMMITTED_SNAPSHOTet SNAPSHOT. Même si aucun de ces niveaux d'isolement n'est activé, le versionnage de ligne est toujours utilisé pour les AFTERdéclencheurs (pour faciliter la génération des insertedet deletedpseudo-tables), MARS et (dans un magasin de versions séparé) l'indexation en ligne.

Comme indiqué , le moteur peut ajouter un suffixe de 14 octets à chaque ligne d'une table qui est versionnée à l'une de ces fins. Ce comportement est relativement bien connu, tout comme l'ajout des données de 14 octets à chaque ligne d'un index qui est reconstruit en ligne avec un niveau d'isolement de version de ligne activé. Même lorsque les niveaux d'isolement ne sont pas activés, un octet supplémentaire est ajouté aux index non cluster uniquement lors de la reconstructionONLINE .

Lorsqu'un déclencheur AFTER est présent et que le versioning ajouterait autrement 14 octets par ligne, une optimisation existe dans le moteur pour éviter cela, mais où une allocation ROW_OVERFLOWor LOBne peut pas se produire. En pratique, cela signifie que la taille maximale possible d'une ligne doit être inférieure à 8060 octets. Dans le calcul du maximum tailles de ligne possibles, le moteur suppose par exemple qu'une colonne VARCHAR (460) peut contenir 460 caractères.

Le comportement est plus facile à voir avec un AFTER UPDATEdéclencheur, bien que le même principe s'applique à AFTER DELETE. Le script suivant crée une table d'une longueur maximale en ligne de 8060 octets. Les données tiennent sur une seule page, avec 13 octets d'espace libre sur cette page. Un déclencheur sans opération existe, donc la page est divisée et des informations de version sont ajoutées:

USE Sandpit;
GO
CREATE TABLE dbo.Example
(
    ID          integer NOT NULL IDENTITY(1,1),
    Value       integer NOT NULL,
    Padding1    char(42) NULL,
    Padding2    varchar(8000) NULL,

    CONSTRAINT PK_Example_ID
    PRIMARY KEY CLUSTERED (ID)
);
GO
WITH
    N1 AS (SELECT 1 AS n UNION ALL SELECT 1),
    N2 AS (SELECT L.n FROM N1 AS L CROSS JOIN N1 AS R),
    N3 AS (SELECT L.n FROM N2 AS L CROSS JOIN N2 AS R),
    N4 AS (SELECT L.n FROM N3 AS L CROSS JOIN N3 AS R)
INSERT TOP (137) dbo.Example
    (Value)
SELECT
    ROW_NUMBER() OVER (ORDER BY (SELECT 0))
FROM N4;
GO
ALTER INDEX PK_Example_ID 
ON dbo.Example 
REBUILD WITH (FILLFACTOR = 100);
GO
SELECT
    ddips.index_type_desc,
    ddips.alloc_unit_type_desc,
    ddips.index_level,
    ddips.page_count,
    ddips.record_count,
    ddips.max_record_size_in_bytes
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID(N'dbo.Example', N'U'), 1, 1, 'DETAILED') AS ddips
WHERE
    ddips.index_level = 0;
GO
CREATE TRIGGER ExampleTrigger
ON dbo.Example
AFTER DELETE, UPDATE
AS RETURN;
GO
UPDATE dbo.Example
SET Value = -Value
WHERE ID = 1;
GO
SELECT
    ddips.index_type_desc,
    ddips.alloc_unit_type_desc,
    ddips.index_level,
    ddips.page_count,
    ddips.record_count,
    ddips.max_record_size_in_bytes
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID(N'dbo.Example', N'U'), 1, 1, 'DETAILED') AS ddips
WHERE
    ddips.index_level = 0;
GO
DROP TABLE dbo.Example;

Le script produit la sortie illustrée ci-dessous. Le tableau d'une seule page est divisé en deux pages et la longueur de ligne physique maximale est passée de 57 à 71 octets (= +14 octets pour les informations de version de ligne).

Exemple de mise à jour

DBCC PAGEmontre que la seule ligne mise à jour a Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71, contrairement à toutes les autres lignes du tableau Record Attributes = NULL_BITMAP; record Size = 57.

Le même script, UPDATEremplacé par une seule ligne, DELETEproduit la sortie affichée:

DELETE dbo.Example
WHERE ID = 1;

Supprimer l'exemple

Il y a une ligne de moins au total (bien sûr!), Mais la taille de ligne physique maximale n'a pas augmenté. Les informations de version de ligne ne sont ajoutées qu'aux lignes nécessaires aux pseudo-tables de déclenchement, et cette ligne a finalement été supprimée. Le partage de page reste cependant. Cette activité de fractionnement de page est responsable de la lenteur des performances observée lorsque le déclencheur était présent. Si la définition de la Padding2colonne passe de varchar(8000)à varchar(7999), la page ne se divise plus.

Voir également ce billet de blog de Dmitri Korotkevitch, MVP de SQL Server, qui traite également de l'impact sur la fragmentation.

Paul White réintègre Monica
la source
1
Ah, j'ai posé une question à ce sujet sur SO il y a quelque temps et je n'ai jamais obtenu de réponse définitive.
Martin Smith
5

Eh bien, voici la réponse officielle de Microsoft ... qui, je pense, est un gros défaut de conception.

14/11/2011 - La réponse officielle a changé. Ils n'utilisent pas le journal des transactions comme indiqué précédemment. Ils utilisent le magasin interne (niveau ligne) pour copier les données modifiées dans. Ils ne peuvent toujours pas déterminer pourquoi cela a pris autant de temps.

Nous avons décidé d'utiliser les déclencheurs Au lieu de au lieu des déclencheurs après suppression.

La partie APRÈS du déclencheur nous oblige à lire le journal des transactions une fois les suppressions terminées et à créer le tableau inséré / supprimé du déclencheur. C'est là que nous passons le plus de temps et c'est par conception pour la partie APRÈS du déclencheur. Le déclencheur INSTEAD OF empêcherait ce comportement d'analyser le journal des transactions et de créer une table insérée / supprimée. De plus, comme il a été observé que les choses sont beaucoup plus rapides si nous supprimons toutes les colonnes avec nvarchar (max), ce qui est logique car elles sont considérées comme des données LOB. Veuillez consulter l'article ci-dessous pour plus d'informations sur les données en ligne:

http://msdn.microsoft.com/en-us/library/ms189087.aspx

Résumé: APRÈS le déclenchement, il faut parcourir le journal des transactions après la fin de la suppression, puis nous devons créer et insérer / supprimer une table, ce qui nécessite davantage d'utilisation du journal des transactions et du temps.

Donc, en tant que plan d'action, voici ce que nous proposons en ce moment:

A) Limit the number of rows deleted in each transaction or
B) Increase timeout settings or
C) Don't use AFTER trigger or trigger at all or
D) Limit usage of nvarchar(max) datatypes.
tsells
la source
2

Selon le plan, tout se passe correctement. Vous pouvez essayer d'écrire la suppression en tant que JOIN au lieu d'un IN, ce qui vous donnera un plan différent.

DELETE m
FROM MAIN m
JOIN Secondary s ON m.ID = s.ValueInt1
AND s.SetTMGUID = '9DDD2C8DD3864EA7B78DA22B2ED572D7'

Je ne sais pas combien cela va aider cependant. Lorsque la suppression est en cours d'exécution avec les déclencheurs sur la table, quel est le type d'attente pour la session effectuant la suppression?

mrdenny
la source