10

私は、システムの 1 つで繰り返される「バグ レポート」(パフォーマンスの問題) を分析しており、特に遅い削除操作に関連しています。簡単に言えば、キーが主な原因であると思われCASCADE DELETEます.(a)これが理にかなっているのか、(b)なぜそうなのかを知りたい.

たとえば、関連テーブルや関連テーブルなどの大きなグラフのルートにあるウィジェットなどのスキーマがあります。完全に明確にするために、このテーブルから削除することは積極的に推奨されていません。それは「核の選択肢」であり、ユーザーは逆に幻想を抱くことはありません。とはいえ、やらなければならないこともあります。

スキーマは次のようになります。

Widgets
   |
   +--- Anvils [1:1]
   |    |
   |    +--- AnvilTestData [1:N]
   |
   +--- WidgetHistory (1:N)
        |
        +--- WidgetHistoryDetails (1:N)

列の定義は次のようになります。

Widgets (WidgetID int PK, WidgetName varchar(50))
Anvils (AnvilID int PK, WidgetID int FK/IX/UNIQUE, ...)
AnvilTestData (AnvilID int FK/IX, TestID int, ...Test Data...)
WidgetHistory (HistoryID int PK, WidgetID int FK/IX, HistoryDate datetime, ...)
WidgetHistoryDetails (HistoryID int FK/IX, DetailType smallint, ...)

怖すぎることはありません。aWidgetは異なる型にすることができ、 anAnvilは特別な型であるため、関係は 1:1 (より正確には 1:0..1) になります。次に、硬度、腐食、正確な重量、ハンマーの互換性、使いやすさの問題、および漫画の頭での衝撃テストを扱う、大量のデータがありますAnvilTestDataAnvil

次に、それぞれWidgetに、生産、在庫の移動、販売、欠陥調査、RMA、修理、顧客からの苦情など、さまざまな種類のトランザクションの長くて退屈な履歴があります。その年齢に応じて。

したがって、当然のことながら、CASCADE DELETEここにはあらゆるレベルで関係があります。を削除する必要がある場合Widget、それは何かがひどく間違っていることを意味し、履歴、テスト データなどを含む、そのウィジェットの既存の記録をすべて消去する必要があります。

関係はすべて索引付けされ、統計は最新です。通常のクエリは高速です。システムは、削除を除くすべての処理で非常にスムーズに動作する傾向があります。

最後に、さまざまな理由から、一度に 1 つのウィジェットしか削除できないため、delete ステートメントは次のようになります。

DELETE FROM Widgets
WHERE WidgetID = @WidgetID

非常にシンプルで無害に見える削除...データのないウィジェットの場合、実行に2分以上かかります!

実行計画をたどった後、最終的にコストが最も高いサブ操作としてAnvilTestDataと削除を選択することができました。WidgetHistoryDetailsそこで、 をオフにしてCASCADE(ただし、実際の FK を に設定するだけでNO ACTION)、スクリプトを次のように書き直して実験しました。

DECLARE @AnvilID int
SELECT @AnvilID = AnvilID FROM Anvils WHERE WidgetID = @WidgetID

DELETE FROM AnvilTestData
WHERE AnvilID = @AnvilID

DELETE FROM WidgetHistory
WHERE HistoryID IN (
    SELECT HistoryID
    FROM WidgetHistory
    WHERE WidgetID = @WidgetID)

DELETE FROM Widgets WHERE WidgetID = @WidgetID

これらの「最適化」はどちらも大幅なスピードアップをもたらし、それぞれが実行時間をほぼ 1 分短縮したため、元の 2 分間の削除は約 5 ~ 10 秒かかりました - 少なくとも新しいウィジェットの場合、多くの履歴やテストは必要ありませんデータ。

完全に明確にするために、ファンアウトが最も高いCASCADEfrom からWidgetHistoryまでがまだあります。ここでは からのものだけを削除しました。WidgetHistoryDetailsWidgets

カスケード関係をさらに「フラット化」すると、徐々に劇的ではなくなりますが、それでも顕著なスピードアップが見られ、より大きなテーブルへのカスケード削除がすべて削除され、明示的な削除に置き換えられると、新しいウィジェットの削除がほぼ瞬時に行われるようになりました。

私は各テストの前にDBCC DROPCLEANBUFFERSandを使用しています。DBCC FREEPROCCACHEさらにスローダウンを引き起こす可能性のあるすべてのトリガーを無効にしました (ただし、それらはいずれにせよ実行計画に表示されます)。また、私は古いウィジェットに対してもテストを行っており、そこでも大幅な高速化が見られます。以前は 5 分かかっていた削除が 20 ~ 40 秒で完了します。

現在、私は「SELECT は壊れていない」という哲学の熱烈な支持者ですが、この動作について論理的な説明はないようですCASCADE DELETE

だから、私の質問は次のとおりです。

  • これは SQL Server の DRI に関する既知の問題ですか? (Google や SO でこの種のことへの言及を見つけることができなかったようです。答えはノーだと思います。)

  • そうでない場合、私が見ている動作について別の説明がありますか?

  • これが既知の問題である場合、なぜそれが問題なのですか? また、使用できるより良い回避策はありますか?

4

1 に答える 1

9

SQL Serverセットベースの操作に最適ですCASCADEが、削除はその性質上、レコードベースです。

SQL Serverは、他のサーバーとは異なり、即時のセットベースの操作を最適化しようとしますが、1 レベルの深さでしか機能しません。下位テーブルのレコードを削除するには、上位テーブルのレコードを削除する必要があります。

言い換えると、カスケード操作は上から下に動作しますが、ソリューションは下から上に動作します。これは、よりセットベースで効率的です。

サンプル スキーマを次に示します。

CREATE TABLE t_g (id INT NOT NULL PRIMARY KEY)

CREATE TABLE t_p (id INT NOT NULL PRIMARY KEY, g INT NOT NULL, CONSTRAINT fk_p_g FOREIGN KEY (g) REFERENCES t_g ON DELETE CASCADE)

CREATE TABLE t_c (id INT NOT NULL PRIMARY KEY, p INT NOT NULL, CONSTRAINT fk_c_p FOREIGN KEY (p) REFERENCES t_p ON DELETE CASCADE)

CREATE INDEX ix_p_g ON t_p (g)

CREATE INDEX ix_c_p ON t_c (p)

、このクエリ:

DELETE
FROM    t_g
WHERE   id > 50000

とその計画:

  |--Sequence
       |--Table Spool
       |    |--Clustered Index Delete(OBJECT:([test].[dbo].[t_g].[PK__t_g__176E4C6B]), WHERE:([test].[dbo].[t_g].[id] > (50000)))
       |--Index Delete(OBJECT:([test].[dbo].[t_p].[ix_p_g]) WITH ORDERED PREFETCH)
       |    |--Sort(ORDER BY:([test].[dbo].[t_p].[g] ASC, [test].[dbo].[t_p].[id] ASC))
       |         |--Table Spool
       |              |--Clustered Index Delete(OBJECT:([test].[dbo].[t_p].[PK__t_p__195694DD]) WITH ORDERED PREFETCH)
       |                   |--Sort(ORDER BY:([test].[dbo].[t_p].[id] ASC))
       |                        |--Merge Join(Inner Join, MERGE:([test].[dbo].[t_g].[id])=([test].[dbo].[t_p].[g]), RESIDUAL:([test].[dbo].[t_p].[g]=[test].[dbo].[t_g].[id]))
       |                             |--Table Spool
       |                             |--Index Scan(OBJECT:([test].[dbo].[t_p].[ix_p_g]), ORDERED FORWARD)
       |--Index Delete(OBJECT:([test].[dbo].[t_c].[ix_c_p]) WITH ORDERED PREFETCH)
            |--Sort(ORDER BY:([test].[dbo].[t_c].[p] ASC, [test].[dbo].[t_c].[id] ASC))
                 |--Clustered Index Delete(OBJECT:([test].[dbo].[t_c].[PK__t_c__1C330188]) WITH ORDERED PREFETCH)
                      |--Table Spool
                           |--Sort(ORDER BY:([test].[dbo].[t_c].[id] ASC))
                                |--Hash Match(Inner Join, HASH:([test].[dbo].[t_p].[id])=([test].[dbo].[t_c].[p]))
                                     |--Table Spool
                                     |--Index Scan(OBJECT:([test].[dbo].[t_c].[ix_c_p]), ORDERED FORWARD)

最初にSQL Serverからレコードを削除しt_g、次に で削除されたレコードを結合t_pして後者から削除し、最後に で削除されたレコードを結合しt_pt_cから削除しt_cます。

この場合、1 つの 3 つのテーブルを結合する方がはるかに効率的であり、回避策としてこれを実行します。

気分が良くなる場合は、Oracleカスケード操作を最適化することはありませんNESTED LOOPS。参照列にインデックスを作成するのを忘れた場合、それらは常に最適化され、神が助けてくれます。

于 2010-03-26T17:25:43.630 に答える