クラスター化列ストアインデックスからのデータの削除をテストしています。
私は実行計画に大きな熱心なスプールオペレーターがあることに気づきました:
これは、次の特性で完了します。
見積もりをだまして過小評価すると、TempDBの使用を回避するより高速な計画が得られます。
スキャンの推定コスト:56.901
(これは推定計画ですが、コメント内の数値は正しいです。)
興味深いことに、次のコマンドを実行してデルタストアをフラッシュすると、スプールが再び消えます。
ALTER INDEX IX_Clustered ON Fact.RecordedMetricsDetail REORGANIZE WITH (COMPRESS_ALL_ROW_GROUPS = ON);
スプールは、デルタストアにしきい値を超えるページがある場合にのみ導入されるようです。
デルタストアのサイズを確認するために、次のクエリを実行して、テーブルの行内ページを確認しています。
SELECT
SUM([in_row_used_page_count]) AS in_row_used_pages,
SUM(in_row_data_page_count) AS in_row_data_pages
FROM sys.[dm_db_partition_stats] as pstats
JOIN sys.partitions AS p
ON pstats.partition_id = p.partition_id
WHERE p.[object_id] = OBJECT_ID('Fact.RecordedMetricsDetail');
最初の計画でスプールイテレータに妥当なメリットはありますか?存在が一貫していないため、これはパフォーマンスの向上を目的としており、ハロウィーンの保護を目的としたものではないと想定する必要があります。
私はこれを2016 CTP 3.1でテストしていますが、2014 SP1 CU3でも同じ動作が見られます。
スキーマとデータを生成し、問題のデモンストレーションを説明するスクリプト here を投稿しました。
質問の原因となった問題(大容量のスプールで満たされたTempDB)の回避策があるので、この時点でのオプティマイザーの動作についての質問はほとんど好奇心から抜けています。代わりにパーティション切り替えを使用して削除しています。
最初の計画でスプールイテレータに妥当なメリットはありますか?
これは、あなたが「もっともらしい」と考えるものに依存しますが、コストモデルによる答えは「はい」です。もちろん、これは真実です。なぜなら、オプティマイザは常に、見つけた最も安いプランを選択するからです。
本当の質問は、なぜコストモデルは、スプールのあるプランを、ないプランよりもはるかに安価であると見なすかです。行がデルタストアに追加される前に、(スクリプトから)新しいテーブル用に作成された推定計画を検討してください。
DELETE Fact.RecordedMetricsDetail
WHERE MeasurementTime < DATEADD(day,-1,GETUTCDATE())
OPTION (RECOMPILE);
このプランの推定コストは巨大です771,734ユニット:
削除によって大量のランダムI/Oが発生すると予想されるため、コストはほぼすべてがクラスター化インデックスの削除に関連しています。これは、すべてのデータ変更に適用される単なる一般的なロジックです。たとえば、Bツリーインデックスへの変更の順序付けされていないセットは、関連する高いI/Oコストとともに、大部分がランダムなI/Oになると想定されます。
データ変更プランでは、これらのコスト上の理由から、順次アクセスを促進する順序で行を表示するための並べ替えを使用できます。この場合、テーブルがパーティション化されているため、影響はさらに大きくなります。実際、非常に分割されています。スクリプトはそれらの15,000を作成します。非常に分割されたテーブルへのランダムな更新は、パーティション(行セット)を途中で切り替えるための価格にも高いコストがかかるため、特に高くつきます。
考慮すべき最後の主な要因は、上記の単純な更新クエリ(「更新」は削除を含むすべてのデータ変更操作を意味します)は、「行セット共有」と呼ばれる最適化の対象となり、スキャンとスキャンの両方に同じ内部行セットが使用されることです。テーブルを更新します。実行プランにはまだ2つの別個の演算子が表示されていますが、それでも使用される行セットは1つだけです。
この最適化を適用できるということは、オプティマイザがランダムなI/Oのコストを削減するためにexplicitlysortingの潜在的な利点を単純に考慮しないコードパスを取ることを意味するため、これについて述べます。テーブルがBツリーである場合、構造は本質的に順序付けられているため、これは理にかなっています。したがって、行セットを共有すると、すべての潜在的な利点が自動的に提供されます。
重要な結果は、更新演算子のコスト計算ロジックは、基になるオブジェクトが列ストアである場合のこの順序付けの利点(シーケンシャルI/Oまたは他の最適化の促進)を考慮しないことです。これは、列ストアの変更がインプレースで実行されないためです。デルタストアを使用します。したがって、コストモデルは、Bツリーと列ストアでの共有行セットの更新の違いを反映しています。
それでも、(非常に!)パーティション化された列ストアの特別なケースでは、次のパーティションに移動する前に1つのパーティションにすべての更新を実行する方がI/Oの観点から有利であるという点で、順序を保持することにはメリットがあるかもしれません。 。
ここでは列ストアに標準のコストロジックが再利用されるため、パーティションの順序を維持する計画(各パーティション内の順序ではない)のコストは低くなります。ドキュメント化されていないトレースフラグ2332を使用してテストクエリでこれを確認し、更新演算子にソートされた入力を要求することができます。これにより、更新時にDMLRequestSort
プロパティがtrueに設定され、オプティマイザは次のパーティションに移動する前に、1つのパーティションにすべての行を提供する計画を作成します。
DELETE Fact.RecordedMetricsDetail
WHERE MeasurementTime < DATEADD(day,-1,GETUTCDATE())
OPTION (RECOMPILE, QUERYTRACEON 2332);
このプランの推定コストは、52.5174単位で非常に低くなります。
このコストの削減はすべて、更新時の推定I/Oコストが低いためです。導入されたSpoolは、DMLRequestSort = true
を使用した更新で要求されるように、パーティション順に出力を保証できることを除いて、有用な機能を実行しません(列ストアインデックスのシリアルスキャンはこの保証を提供できません)。スプール自体のコストは、特に更新時のコストの(おそらく非現実的な)削減と比較して、比較的低いと考えられています。
更新演算子への順序付けされた入力が必要かどうかの決定は、クエリ最適化の非常に早い段階で行われます。この決定で使用されたヒューリスティックはこれまでに文書化されていませんが、試行錯誤によって決定できます。デルタストアのサイズは、この決定への入力のようです。一度選択すると、クエリのコンパイルに対して永続的な選択になります。 USE PLAN
hintは成功しません。計画のターゲットは、更新への入力を注文したか、注文しませんでした。
カーディナリティの見積もりを人為的に制限することなく、このクエリの低コストの計画を取得する別の方法があります。スプールを回避するために十分に低い見積もりでは、おそらくDMLRequestSortがfalseになり、予想されるランダムI/Oのために非常に高い見積もり計画コストが発生します。別の方法は、トレースフラグ8649(並列プラン)を2332(DMLRequestSort = true)と組み合わせて使用することです。
DELETE Fact.RecordedMetricsDetail
WHERE MeasurementTime < DATEADD(day,-1,GETUTCDATE())
OPTION (RECOMPILE, QUERYTRACEON 2332, QUERYTRACEON 8649);
これにより、パーティションごとのバッチモードの並列スキャンと順序を維持する(マージする)Gather Streams交換を使用する計画が作成されます。
ハードウェアでのパーティションの順序の実行時の有効性に応じて、これは3つのうち最高のパフォーマンスを発揮します。とは言っても、大きな変更は列ストアの優れたアイデアではないため、パーティション切り替えのアイデアはほぼ間違いなく優れています。長いコンパイル時間と、パーティション化されたオブジェクトでよく見られる風変わりな計画の選択に対処できる場合-特にパーティションの数が多い場合。
比較的新しい多くの機能を、特に制限の近くで組み合わせると、質の悪い実行計画を取得するための優れた方法になります。オプティマイザサポートの深さは時間とともに改善する傾向がありますが、列ストアの15,000パーティションを使用すると、常に興味深い時代に生きることを意味します。