web-dev-qa-db-ja.com

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

マスターテーブルを詳細テーブルに結合するときに、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(別名インデックスシークオペレーター)を有効にするためにあります。

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

enter image description here

同じ問題のあるクエリの実際のプランは here で見つかります。

ご覧のように、結合(図の左下にあるツールチップ)のカーディナリティー推定は4倍を超えて低すぎます。実際の推定210万対推定0.5 Mです。これにより、特にこのクエリがより複雑なクエリで使用されるサブクエリである場合に、パフォーマンスの問題(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がその等式ヒントを使用していないか、何か他の問題が問題の根本的な原因です。

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

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

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

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

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

9
Justin Grant

統計に何かを行うか、レガシーCEを使用しても改善が得られないと仮定すると、問題を回避する最も簡単な方法は、INNER JOINLEFT 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の提案: メタデータを使用してオプティマイザの見積もりをより正確にします )。

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

6
Joe Obbish