マスターテーブルを詳細テーブルに結合するときに、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を超える行が返されます。