マスター/詳細テーブル間のハッシュ結合により、カーディナリティの見積もりが低すぎる


9

マスターテーブルを詳細テーブルに結合するときに、SQL Server 2014が大きい(詳細)テーブルの基数推定を結合出力の基数推定として使用するようにするにはどうすればよいですか?

たとえば、10Kのマスター行を100Kの詳細行に結合する場合、SQL Serverで結合を100K行で推定します-詳細行の推定数と同じです。すべての詳細行に常に対応するマスター行があるという事実をSQL Serverの推定器が活用できるようにするには、クエリやテーブル、インデックス、あるいはその両方をどのように構成すればよいですか?(それらの間の結合は、カーディナリティの推定値を決して減らすべきではないという意味です。)

詳細はこちらです。このデータベースには、マスター/詳細のテーブルのペアがありVisitTargetます。販売トランザクションごとに1つの行があり、トランザクションVisitSaleごとに製品ごとに1つの行があります。これは1対多の関係です。VisitTargetの行が1つで、平均で10件のVisitSale行があります。

テーブルは次のようになります(この質問に関連する列のみを簡略化しています)。

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

パフォーマンス上の理由から、最も一般的なフィルタリング列(などSaleDate)をマスターテーブルから各詳細テーブルの行にコピーすることで部分的に非正規化し、日付フィルターされたクエリをより適切にサポートするために両方のテーブルにカバーインデックスを追加しました。これは、日付でフィルター処理されたクエリを実行するときのI / Oを減らすのに効果的ですが、マスターと詳細テーブルを結合するときに、このアプローチがカーディナリティの推定問題を引き起こしていると思います。

これら2つのテーブルを結合すると、クエリは次のようになります。

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

詳細テーブル(VisitSale)の日付フィルターは冗長です。日付範囲でフィルターされたクエリの詳細テーブルで順次I / O(別名Index Seekオペレーター)を有効にするためにあります。

これらの種類のクエリの計画は次のようになります。

ここに画像の説明を入力してください

同じ問題のあるクエリの実際の計画は、ここにあります

ご覧のように、結合(図の左下にあるツールチップ)のカーディナリティー推定は4倍を超えて低すぎます。実際の推定210万に対して推定0.5百万です。これにより、特にこのクエリがより複雑なクエリで使用されるサブクエリである場合に、パフォーマンスの問題(tempdbへの流出など)が発生します。

ただし、結合の各ブランチの行数の見積もりは、実際の行数に近いです。結合の上半分は、実際の100K対推定164Kです。ジョインの下半分は、実際には210万行であるのに対し、推定では370万行です。ハッシュバケットの分散も適切に見えます。これらの観察から、統計は各テーブルで問題なく、問題は結合のカーディナリティの推定であることがわかります。

最初、問題はSQL Serverであり、各テーブルのSaleDate列は独立しているが、実際には同じであることを期待していると考えました。そこで、結合条件またはWHERE句にSale日付の等価比較を追加してみました。たとえば、

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

または

WHERE vt.SaleDate = vs.SaleDate

これはうまくいきませんでした。カーディナリティの見積もりがさらに悪化しました!したがって、SQL Serverがその等式ヒントを使用していないか、何か他のものが問題の根本的な原因です。

このカーディナリティ推定の問題をトラブルシューティングし、うまくいけば修正する方法に関するアイデアはありますか?私の目標は、マスター/詳細結合のカーディナリティーを、結合のより大きな(「詳細テーブル」)入力の推定と同じように推定することです。

問題がある場合は、Windows Server上でSQL Server 2014 Enterprise SP2 CU8ビルド12.0.5557.0を実行しています。有効になっているトレースフラグはありません。データベースの互換性レベルはSQL Server 2014です。複数の異なるSQL Serverで同じ動作が見られるため、サーバー固有の問題ではないようです。

SQL Server 2014 Cardinality Estimatorには、まさに私が探している動作である最適化があります

ただし、新しいCEは、大きなテーブルと小さなテーブルの間に1対多の結合の関連付けがあると想定する、より単純なアルゴリズムを使用します。これは、大きなテーブルの各行が小さなテーブルの1つの行と正確に一致することを前提としています。このアルゴリズムは、より大きな入力の推定サイズを結合カーディナリティとして返します。

理想的には、この動作が得られ、結合のカーディナリティの見積もりは大きなテーブルの見積もりと同じになりますが、「小さな」テーブルでも100Kを超える行が返されます。

回答:


6

統計に何かを行うか、レガシーCEを使用しても改善が得られないと仮定すると、問題を回避する最も簡単な方法は、をに変更するINNER JOINことLEFT OUTER JOINです。

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

テーブル間に外部キーがある場合、常にSaleDate両方のテーブルに対して同じ範囲でフィルターをかけ、テーブルSaleDate間で常に一致する場合、クエリの結果は変更されません。このような外部結合を使用するのは奇妙に思えるかもしれVisitTargetませんが、テーブルへの結合によってクエリによって返される行の数が減ることは決してないことをクエリオプティマイザーに通知するものと考えてください。残念ながら、外部キーはカーディナリティの見積もりを変更しないため、このようなトリックに頼る必要がある場合があります。(Microsoft Connectの提案:メタデータを使用して、オプティマイザーの見積もりをより正確にします)。

結合後のクエリで他に何が発生するかによっては、このフォームでクエリを作成してもうまく機能しない可能性があります。一時テーブルを使用して、最も重要なカーディナリティの見積もりを含む結果セットの中間結果を保持することができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.