Langsames Löschen von Datensätzen, wenn ein Trigger aktiviert ist


17

Dachte, dies wurde mit dem folgenden Link behoben - die Umgehung funktioniert - aber der Patch funktioniert nicht. Arbeiten mit dem Microsoft-Support zur Lösung.

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

Ok, ich habe ein Problem, das ich StackOverflow mitteilen wollte, um zu sehen, ob jemand eine Idee hat.

Beachten Sie, dass dies mit SQL Server 2008 R2 möglich ist

Problem: Das Löschen von 3000 Datensätzen aus einer Tabelle mit 15000 Datensätzen dauert 3-4 Minuten, wenn ein Trigger aktiviert ist, und nur 3-5 Sekunden, wenn der Trigger deaktiviert ist.

Tischaufbau

Zwei Tabellen nennen wir Main und Secondary. Sekundär enthält Datensätze von Elementen, die ich löschen möchte. Wenn ich den Löschvorgang durchführe, verbinde ich mich mit der Sekundärtabelle. Vor der Anweisung delete wird ein Prozess ausgeführt, um die Sekundärtabelle mit zu löschenden Datensätzen zu füllen.

Anweisung löschen:

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

Diese Tabelle enthält viele Spalten und ungefähr 14 verschiedene NC-Indizes. Ich habe eine Reihe verschiedener Dinge ausprobiert, bevor ich feststellte, dass der Auslöser das Problem war.

  • Seitensperre aktivieren (standardmäßig deaktiviert)
  • Manuell erfasste Statistiken
  • Deaktivierte automatische Erfassung von Statistiken
  • Geprüfter Indexzustand und Fragmentierung
  • Löschte den Clustered-Index aus der Tabelle
  • Untersuchte den Ausführungsplan (nichts wurde als fehlender Index angezeigt, und die Kosten betrugen 70 Prozent für das tatsächliche Löschen, wobei etwa 28 Prozent für das Zusammenführen der Datensätze anfielen

Löst aus

Die Tabelle enthält drei Trigger (jeweils einen für Einfüge-, Aktualisierungs- und Löschvorgänge). Ich habe den Code für den Löschtrigger geändert, um nur zurückzukehren und dann einen auszuwählen, um zu sehen, wie oft er ausgelöst wird. Es wird während des gesamten Vorgangs (wie erwartet) nur einmal ausgelöst.

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

Zu rekapitulieren

  • Bei aktiviertem Trigger dauert die Ausführung der Anweisung 3-4 Minuten
  • With Trigger off - Die Ausführung der Anweisung dauert 3-5 Sekunden

Hat jemand eine Idee warum?

Beachten Sie auch, dass Sie diese Architektur nicht ändern, Indizes entfernen usw. als Lösung hinzufügen möchten. Diese Tabelle ist das zentrale Element für einige wichtige Datenoperationen, und wir mussten sie optimieren und optimieren (Indizes, Seitensperren usw.), damit wichtige parallele Operationen ohne Deadlocks ausgeführt werden können.

Hier ist das xml des Ausführungsplans (die Namen wurden geändert, um die Unschuldigen zu schützen)

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

Antworten:


12

Das in SQL Server 2005 eingeführte Framework für die Zeilenversionierung unterstützt eine Reihe von Funktionen, einschließlich der neuen Transaktionsisolationsstufen READ_COMMITTED_SNAPSHOTund SNAPSHOT. Selbst wenn keine dieser Isolationsstufen aktiviert ist, wird die Zeilenversionierung weiterhin für AFTERTrigger (um die Generierung der insertedund deletedPseudotabellen zu erleichtern ), MARS und (in einem separaten Versionsspeicher) für die Online-Indizierung verwendet.

Wie dokumentiert , kann die Engine jeder Zeile einer Tabelle, die für einen dieser Zwecke versioniert ist, einen 14-Byte-Postfix hinzufügen. Dieses Verhalten ist relativ bekannt, ebenso wie das Hinzufügen der 14-Byte-Daten zu jeder Zeile eines Index, der online mit aktivierter Isolationsstufe für die Zeilenversionierung neu erstellt wird. Selbst wenn die Isolationsstufen nicht aktiviert sind, wird nicht gruppierten Indizes nur dann ein zusätzliches Byte hinzugefügt, wenn sie neu erstellt werden ONLINE.

Wenn ein AFTER-Trigger vorhanden ist und die Versionsverwaltung andernfalls 14 Byte pro Zeile hinzufügt, ist innerhalb der Engine eine Optimierung vorhanden, um dies zu vermeiden , bei der jedoch keine ROW_OVERFLOWoder LOB-Zuweisung erfolgen kann. In der Praxis bedeutet dies, dass die maximal mögliche Größe einer Zeile weniger als 8060 Byte betragen muss. Bei der Berechnung der maximal möglichen Zeilengröße geht die Engine beispielsweise davon aus, dass eine VARCHAR (460) -Spalte 460 Zeichen enthalten könnte.

Das Verhalten ist am einfachsten mit einem AFTER UPDATEAuslöser zu sehen, obwohl das gleiche Prinzip gilt AFTER DELETE. Das folgende Skript erstellt eine Tabelle mit einer maximalen Zeilenlänge von 8060 Byte. Die Daten passen auf eine einzelne Seite mit 13 Byte freiem Speicherplatz auf dieser Seite. Ein No-Op-Trigger ist vorhanden, daher wird die Seite geteilt und es werden Versionsinformationen hinzugefügt:

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;

Das Skript erzeugt die unten gezeigte Ausgabe. Die einseitige Tabelle ist in zwei Seiten unterteilt, und die maximale physische Zeilenlänge wurde von 57 auf 71 Byte erhöht (= +14 Byte für die Zeilenversionsinformationen).

Beispiel aktualisieren

DBCC PAGEzeigt, dass die einzelne aktualisierte Zeile hat Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71, während alle anderen Zeilen in der Tabelle haben Record Attributes = NULL_BITMAP; record Size = 57.

Das gleiche Skript mit der UPDATEdurch eine einzelne Zeile ersetzten DELETEerzeugt die folgende Ausgabe:

DELETE dbo.Example
WHERE ID = 1;

Beispiel löschen

Insgesamt ist (natürlich!) Eine Zeile weniger vorhanden, aber die maximale physische Zeilengröße wurde nicht erhöht. Zeilenversionsinformationen werden nur zu Zeilen hinzugefügt, die für die Trigger-Pseudotabellen benötigt werden, und diese Zeile wurde letztendlich gelöscht. Der Seitenumbruch bleibt jedoch bestehen. Diese Seitenaufteilungsaktivität ist für die langsame Leistung verantwortlich, die beobachtet wurde, als der Auslöser vorhanden war. Wenn die Definition der Padding2Spalte von varchar(8000)nach geändert wird , varchar(7999)wird die Seite nicht mehr geteilt.

Lesen Sie auch diesen Blog-Beitrag von SQL Server MVP Dmitri Korotkevitch, in dem auch die Auswirkungen auf die Fragmentierung erörtert werden.


1
Ah, ich habe vor einiger Zeit eine Frage dazu auf SO gestellt und nie eine endgültige Antwort bekommen.
Martin Smith

5

Nun, hier ist die offizielle Antwort von Microsoft ... die meiner Meinung nach ein großer Konstruktionsfehler ist.

14.11.2011 - Offizielle Antwort wurde geändert. Sie verwenden das Transaktionsprotokoll nicht wie zuvor angegeben. Sie verwenden den internen Speicher (Zeilenebene), um die geänderten Daten in zu kopieren. Sie können immer noch nicht feststellen, warum es so lange gedauert hat.

Wir haben beschlossen, anstelle von Löschtriggern anstelle von Löschtriggern zu verwenden.

Der AFTER-Teil des Triggers veranlasst uns, das Transaktionslog zu lesen, nachdem die Löschvorgänge abgeschlossen sind, und die eingefügte / gelöschte Triggertabelle zu erstellen. Hier verbringen wir die meiste Zeit und dies ist beabsichtigt für den NACH-Teil des Auslösers. Der INSTEAD OF-Trigger würde dieses Verhalten beim Durchsuchen des Transaktionsprotokolls und beim Erstellen einer eingefügten / gelöschten Tabelle verhindern. Außerdem wurde festgestellt, dass die Dinge viel schneller sind, wenn wir alle Spalten mit nvarchar (max) löschen. Dies ist sinnvoll, da es sich um LOB-Daten handelt. Weitere Informationen zu In-Row-Daten finden Sie im folgenden Artikel:

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

Zusammenfassung: Nach Abschluss des Löschvorgangs muss der Trigger das Transaktionsprotokoll erneut durchsuchen. Anschließend müssen wir eine Tabelle erstellen und einfügen / löschen, was eine stärkere Nutzung des Transaktionsprotokolls und der Zeit erfordert.

Als Aktionsplan schlagen wir zur Zeit Folgendes vor:

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.

2

Nach Plan läuft alles richtig. Sie können versuchen, den Löschvorgang als JOIN anstelle eines IN zu schreiben, wodurch Sie einen anderen Plan erhalten.

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

Ich bin mir jedoch nicht sicher, wie viel das helfen wird. Wenn der Löschvorgang mit den Auslösern in der Tabelle ausgeführt wird, welche Art von Wartezeit besteht für die Sitzung, die den Löschvorgang ausführt?

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.