Cancellazione lenta dei record quando è abilitato un trigger


17

Pensavo che questo fosse risolto con il link qui sotto - il lavoro intorno funziona - ma la patch no. Lavorare con il supporto Microsoft per risolvere.

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

Ok, quindi ho un problema che volevo buttare su StackOverflow per vedere se qualcuno ha un'idea.

Nota che questo è con SQL Server 2008 R2

Problema: l'eliminazione di 3000 record da una tabella con 15000 record richiede 3-4 minuti quando un trigger è abilitato e solo 3-5 secondi quando il trigger è disabilitato.

Impostazione della tabella

Due tavoli che chiameremo Main e Secondary. Secondario contiene record di elementi che voglio eliminare, quindi quando eseguo l'eliminazione mi unisco alla tabella Secondaria. Un processo viene eseguito prima dell'istruzione delete per popolare la tabella secondaria con i record da eliminare.

Elimina dichiarazione:

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

Questa tabella ha molte colonne e circa 14 diversi indici NC. Ho provato un sacco di cose diverse prima di stabilire che il problema fosse il trigger.

  • Attiva blocco pagine (per impostazione predefinita abbiamo disattivato)
  • Statistiche raccolte manualmente
  • Raccolta automatica di statistiche disabilitata
  • Integrità e frammentazione dell'indice verificato
  • Eliminato l'indice cluster dalla tabella
  • Esaminato il piano di esecuzione (nulla che mostrava come indici mancanti e il costo era del 70 percento rispetto all'eliminazione effettiva con circa il 28 percento per l'unione / unione dei record

trigger

La tabella ha 3 trigger (uno ciascuno per le operazioni di inserimento, aggiornamento ed eliminazione). Ho modificato il codice per il trigger di eliminazione per restituirlo, quindi selezionarne uno per vedere quante volte viene attivato. Spara solo una volta durante l'intera operazione (come previsto).

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

Ricapitolando

  • Con Trigger on - il completamento dell'istruzione richiede 3-4 minuti
  • Con Trigger disattivato, il completamento dell'istruzione richiede 3-5 secondi

Qualcuno ha qualche idea sul perché?

Nota anche: non cercare di cambiare questa architettura, aggiungere rimuovere indici, ecc. Come soluzione. Questa tabella è l'elemento centrale di alcune importanti operazioni sui dati e abbiamo dovuto modificarlo e ottimizzarlo (indici, blocco delle pagine, ecc.) Per consentire alle principali operazioni di concorrenza di funzionare senza deadlock.

Ecco il piano di esecuzione xml (i nomi sono stati cambiati per proteggere gli innocenti)

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

Risposte:


12

Il framework di versione delle righe introdotto in SQL Server 2005 viene utilizzato per supportare una serie di funzionalità, inclusi i nuovi livelli di isolamento delle transazioni READ_COMMITTED_SNAPSHOTe SNAPSHOT. Anche quando nessuno di questi livelli di isolamento sono abilitati, righe delle versioni viene ancora utilizzato per AFTERtrigger (ad agevolare la generazione del insertede deletedpseudotabelle), MARS, e (in un negozio versione separata) indicizzazione linea.

Come documentato , il motore può aggiungere un postfix di 14 byte a ciascuna riga di una tabella che è stata versionata per uno di questi scopi. Questo comportamento è relativamente noto, così come l'aggiunta dei dati a 14 byte a ogni riga di un indice che viene ricostruita online con un livello di isolamento di versione delle righe abilitato. Anche se i livelli di isolamento non sono abilitati, un solo byte aggiuntivo viene aggiunto agli indici non cluster solo quando ricostruito ONLINE.

Dove è presente un trigger AFTER e delle versioni altrimenti aggiungere 14 byte per riga, un'ottimizzazione esiste all'interno del motore per evitare questo, ma dove una ROW_OVERFLOWo LOBallocazione non può verificarsi. In pratica, ciò significa che la dimensione massima possibile di una riga deve essere inferiore a 8060 byte. Nel calcolare le dimensioni massime possibili delle righe, il motore presuppone, ad esempio, che una colonna VARCHAR (460) possa contenere 460 caratteri.

Il comportamento è più facile da vedere con un AFTER UPDATEtrigger, anche se si applica lo stesso principio AFTER DELETE. Lo script seguente crea una tabella con una lunghezza massima nella riga di 8060 byte. I dati si inseriscono in una singola pagina, con 13 byte di spazio libero su quella pagina. Esiste un trigger no-op, quindi la pagina viene suddivisa e vengono aggiunte informazioni sulla versione:

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;

Lo script produce l'output mostrato di seguito. La tabella a pagina singola è divisa in due pagine e la lunghezza massima della riga fisica è aumentata da 57 a 71 byte (= +14 byte per le informazioni sulla versione delle righe).

Esempio di aggiornamento

DBCC PAGEmostra che la singola riga aggiornata ha Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71, mentre tutte le altre righe nella tabella hanno Record Attributes = NULL_BITMAP; record Size = 57.

Lo stesso script, con il UPDATEsostituito da una singola riga DELETEproduce l'output mostrato:

DELETE dbo.Example
WHERE ID = 1;

Elimina esempio

C'è una riga in meno in totale (ovviamente!), Ma la dimensione massima della riga fisica non è aumentata. Le informazioni sul controllo delle versioni delle righe vengono aggiunte solo alle righe necessarie per le pseudo-tabelle trigger e tale riga è stata infine eliminata. La divisione della pagina rimane, tuttavia. Questa attività di divisione della pagina è responsabile delle prestazioni lente osservate quando era presente il trigger. Se la definizione della Padding2colonna viene modificata da varchar(8000)a varchar(7999), la pagina non si divide più.

Vedi anche questo post sul blog di SQL Server MVP Dmitri Korotkevitch, che discute anche dell'impatto sulla frammentazione.


1
Ah, qualche volta fa ho fatto una domanda al riguardo e non ho mai avuto una risposta definitiva.
Martin Smith,

5

Bene, ecco la risposta ufficiale di Microsoft ... che penso sia un grosso difetto di progettazione.

14/11/2011 - La risposta ufficiale è cambiata. Non stanno utilizzando il registro delle transazioni come precedentemente indicato. Stanno usando l'archivio interno (a livello di riga) per copiare i dati modificati in. Non riescono ancora a capire perché ci sia voluto così tanto tempo.

Abbiamo deciso di utilizzare i trigger Invece di anziché i trigger dopo l'eliminazione.

La parte DOPO del trigger ci obbliga a leggere il registro delle transazioni dopo che le eliminazioni sono state completate e che è stata creata la tabella inserita / eliminata del trigger. Qui è dove trascorriamo molto tempo ed è progettato per la parte DOPO il grilletto. Il trigger INSTEAD OF impedirebbe questo comportamento di scansionare il registro delle transazioni e costruire una tabella inserita / eliminata. Inoltre, come è stato osservato che le cose sono molto più veloci se eliminiamo tutte le colonne con nvarchar (max), il che ha senso a causa del fatto che sono considerati dati LOB. Leggi l'articolo seguente per ulteriori informazioni sui dati In-Row:

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

Riepilogo: il trigger AFTER richiede la scansione indietro nel registro delle transazioni al termine dell'eliminazione, quindi è necessario creare una tabella inserita / inserita che richieda un maggiore utilizzo del registro delle transazioni e del tempo.

Quindi, come piano d'azione, questo è ciò che suggeriamo in questo momento:

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

Secondo il piano tutto procede correttamente. Puoi provare a scrivere la cancellazione come JOIN anziché IN, che ti darà un piano diverso.

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

Non sono sicuro di quanto ciò possa aiutare comunque. Quando l'eliminazione è in esecuzione con i trigger sul tavolo, qual è il tipo di attesa per la sessione che esegue l'eliminazione?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.