web-dev-qa-db-ja.com

トリガーが有効になっているときのレコードの削除が遅い

これは以下のリンクで解決されたと思います-回避策は機能しますが、パッチは機能しません。マイクロソフトサポートと協力して解決する。

http://support.Microsoft.com/kb/260688

わかりましたので、誰かがアイデアを持っているかどうかを確認するためにStackOverflowに投げたい問題があります。

これはSQL Server 2008 R2であることに注意してください

問題:15000レコードのテーブルから3000レコードを削除すると、トリガーが有効な場合は3〜4分かかり、トリガーが無効な場合は3〜5秒しかかかりません。

テーブル設定

MainとSecondaryと呼ぶ2つのテーブル。セカンダリには削除したいアイテムのレコードが含まれているため、削除を実行するとセカンダリテーブルに結合します。 deleteステートメントの前にプロセスが実行され、セカンダリテーブルに削除対象のレコードが読み込まれます。

Deleteステートメント:

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

このテーブルには多くの列と約14の異なるNCインデックスがあります。トリガーが問題であると判断する前に、さまざまなことを試しました。

  • ページロックをオンにする(デフォルトではオフになっています)
  • 統計を手動で収集
  • 統計の自動収集を無効にしました
  • 検証済みインデックスの状態と断片化
  • テーブルからクラスター化インデックスを削除しました
  • 実行計画を調べました(インデックスが欠落していることを示すものはなく、コストは実際の削除に対して70%で、レコードの結合/マージでは約28%でした)

トリガー

テーブルには3つのトリガーがあります(挿入、更新、および削除操作ごとに1つ)。削除トリガーのコードを変更して、戻るだけのトリガーを選択して、トリガーが何回起動されるかを確認しました。オペレーション全体で1回だけ発生します(予想どおり)。

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

要約するには

  • トリガーがオンの場合-ステートメントの完了には3〜4分かかります
  • トリガーがオフの場合-ステートメントの完了には3〜5秒かかります

誰もが理由について何か考えがありますか?

また、解決策として、このアーキテクチャを変更したり、インデックスを削除したりすることなどを考えていません。このテーブルはいくつかの主要なデータ操作の中心部分であり、主要な同時実行操作がデッドロックなしで機能できるように、それを微調整および調整(インデックス、ページロックなど)する必要がありました。

実行計画のxmlは次のとおりです(無実を保護するために名前が変更されました)

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

さて、ここにマイクロソフトからの公式の返答があります...これは設計上の大きな欠陥だと思います。

2011年11月14日-公式の対応が変更されました。前述のトランザクションログは使用していません。内部ストア(行レベル)を使用して、変更されたデータをコピーします。彼らはまだそれがなぜそんなに長くかかるのかを決定することができません。

削除後トリガーの代わりに、代わりにトリガーを使用することにしました。

トリガーのAFTER部分により、削除が完了した後にトランザクションログを読み取り、トリガーの挿入/削除されたテーブルを構築する必要があります。これは、私たちが膨大な時間を費やす場所であり、トリガーのAFTER部分の設計によるものです。 INSTEAD OFトリガーは、トランザクションログのスキャンと挿入/削除されたテーブルの作成というこの動作を防止します。また、nvarchar(max)を使用してすべての列を削除した方がはるかに高速であることが観察されたため、LOBデータと見なされるため、これは理にかなっています。行内データに関する詳細については、以下の記事をご覧ください。

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

概要:AFTERトリガーでは、削除が完了した後、トランザクションログをスキャンして戻す必要があります。その後、トランザクションログと時間の使用量を増やす必要があるテーブルを作成して挿入/削除する必要があります。

したがって、行動計画として、これは現時点で私たちが提案するものです:

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.
5
tsells

SQL Server 2005で導入された行バージョン管理フレームワークは、新しいトランザクション分離レベル_READ_COMMITTED_SNAPSHOT_およびSNAPSHOTを含む多数の機能をサポートするために使用されます。これらの分離レベルがどちらも有効になっていない場合でも、AFTERトリガー(insertedおよびdeleted疑似テーブルの生成を容易にするため)、MARS、 (別のバージョンストアで)オンラインインデックス。

documented として、エンジンは、これらの目的のいずれかのためにバージョン管理されているテーブルの各行に14バイトの接尾辞を追加できます。この動作は比較的よく知られており、行バージョンの分離レベルが有効になっている オンラインで再構築 であるインデックスのすべての行に14バイトのデータが追加されます。分離レベルが有効になっていない場合でも、ONLINEを再構築すると、 非クラスター化インデックスのみ に1バイト追加されます。

AFTERトリガーが存在し、バージョニングが行ごとに14バイトを追加する場合、これをavoidするためにエンジン内に最適化が存在しますが、_ROW_OVERFLOW_またはLOB割り当ては発生しません。実際には、これは行の可能な最大サイズが8060バイト未満でなければならないことを意味します。 maximum可能な行サイズを計算する際、エンジンは、たとえばVARCHAR(460)列に460文字が含まれる可能性があると想定します。

_AFTER UPDATE_にも同じ原理が適用されますが、動作は_AFTER DELETE_トリガーで最も簡単に確認できます。次のスクリプトは、行内の最大長が8060バイトのテーブルを作成します。データは1ページに収まり、そのページには13バイトの空き領域があります。 no-opトリガーが存在するため、ページが分割され、バージョン情報が追加されます。

_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;
_

スクリプトは、以下に示す出力を生成します。単一ページのテーブルが2ページに分割され、最大physical行の長さが57バイトから71バイト(= +14バイトの行)に増加しました-バージョン情報)。

Update example

_DBCC PAGE_は、更新された単一の行に_Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71_があるのに対し、テーブル内の他のすべての行には_Record Attributes = NULL_BITMAP; record Size = 57_があることを示しています。

UPDATEを単一の行DELETEに置き換えた同じスクリプトは、次の出力を生成します。

_DELETE dbo.Example
WHERE ID = 1;
_

Delete example

合計で1行少ない(もちろん!)が、物理的な最大行サイズは増えていません。行のバージョン管理情報は、トリガーの疑似テーブルに必要な行にのみ追加され、その行は最終的に削除されました。ただし、ページ分割は残ります。このページ分割アクティビティは、トリガーが存在したときに観察されたパフォーマンスの低下を引き起こします。 _Padding2_列の定義がvarchar(8000)からvarchar(7999)に変更された場合、ページは分割されなくなります。

また、SQL Server MVPのDmitri Korotkevitchによる ブログ投稿 も参照してください。フラグメント化への影響についても説明しています。

12
Paul White 9

計画によると、すべてが正しく進んでいます。 INの代わりにJOINとして削除を書き込んでみてください。これにより、別のプランが提供されます。

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

しかし、それがどれだけ役立つかはわかりません。テーブルのトリガーで削除が実行されている場合、削除を実行しているセッションの待機タイプは何ですか?

2
mrdenny