web-dev-qa-db-ja.com

この結合のカーディナリティの見積もりがそれほど大きいのはなぜですか?

次のクエリのカーディナリティの推定値が非常に高いと思います。

SELECT dm.PRIMARY_ID
FROM
(
    SELECT COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;

見積もりは こちら です。テーブルの統計コピーを作成しているので、実際の計画を含めることはできません。ただし、この問題にはあまり関係がないと思います。

SQL Serverは、 "dm"派生テーブルから481577行が返されると推定しています。次に、X_LAST_TABLEへの結合が実行された後に4528030000行が返されると推定しますが、JOIN_IDはX_LAST_TIMEの主キーです。結合カーディナリティの推定値は0〜481577行になると思います。代わりに、行の見積もりは、外部テーブルと内部テーブルをクロス結合するときに取得する行数の10%のように見えます。この計算は、四捨五入でうまくいきます:481577 * 94025 * 0.1 = 45280277425これは、4528030000に丸められます。

私は主にこの動作の根本的な原因を探しています。簡単な回避策にも興味がありますが、データモデルの変更や一時テーブルの使用はお勧めしません。このクエリは、ビュー内のロジックを単純化したものです。いくつかの列でCOALESCEを実行し、それらを結合することは良い習慣ではないことを知っています。この質問の目的の一部は、データモデルの再設計を推奨する必要があるかどうかを判断することです。

従来のカーディナリティエスティメータを有効にして、Microsoft SQL Server 2014でテストしています。 TF 4199とその他の機能は有効です。最終的に関連性がある場合は、トレースフラグの完全なリストを提供できます。

最も関連性の高いテーブル定義は次のとおりです。

CREATE TABLE X_LAST_TABLE (
JOIN_ID NUMERIC(18, 0) NOT NULL
    CONSTRAINT PK_X_LAST_TABLE PRIMARY KEY CLUSTERED (JOIN_ID ASC)
);

また、誰かがサーバーの1つで問題を再現したい場合は、 統計とともにすべてのテーブル作成スクリプトをスクリプト化 します。

私の観察のいくつかを追加するために、TF 2312を使用すると推定値が修正されますが、それは私には選択肢ではありません。 TF 2301は推定値を修正しません。テーブルの1つを削除すると、見積もりが修正されます。奇妙なことに、X_DETAIL_LINKの結合順序を変更すると、見積もりも修正されます。結合順序を変更するということは、クエリを書き換え、ヒントで結合順序を強制しないことを意味します。次に、結合の順序を変更しただけの 推定クエリプラン を示します。

19
Joe Obbish

いくつかの列でCOALESCEを実行してそれらを結合することは良い習慣ではないことを知っています。

スキーマが3NF +(キーと制約付き)であり、クエリがリレーショナルであり、主にSPJG(selection-projection-join-group by)である場合、適切なカーディナリティと分布推定値を生成するのは十分困難です。 CEモデルはこれらの原則に基づいて構築されています。より多くのunusualまたは非リレーショナル機能がクエリにあり、カーディナリティと選択性のフレームワークが処理できるものの境界に近づくほどです。行き過ぎるとCEは諦めて推測

MCVEの例のほとんどは単純なSPJ(Gなし)ですが、単純な内部等価結合(または準結合)ではなく、主に外部等価結合(内部結合と反半結合としてモデル化)があります。すべての関係にはキーがありますが、外部キーやその他の制約はありません。結合の1つを除くすべてが1対多であり、これは良いことです。

例外は、X_DETAIL_1X_DETAIL_LINKの間の多対多外部結合です。 MCVEでのこの結合の唯一の機能は、X_DETAIL_1の行を潜在的に複製することです。これは珍しい種類のものです。

単純な等価述語(選択)とスカラー演算子も優れています。たとえば、attribute compare-equal attribute/constantは通常、モデルで適切に機能します。このような述語の適用を反映するようにヒストグラムと頻度統計を変更することは比較的「簡単」です。

COALESCECASEに基づいて構築されており、内部でIIFとして実装されています(これは、Transact-SQL言語でIIFが登場するずっと前からありました)。 。 CEはIIFを、相互に排他的な2つの子を持つUNIONとしてモデル化します。各子は、入力リレーションの選択に関するプロジェクトで構成されます。リストされている各コンポーネントはモデルをサポートしているため、それらを組み合わせるのは比較的簡単です。それでも、1層の抽象化が多ければ多いほど、最終結果の精度は低くなる傾向があります。これは、実行プランが大きいほど、安定性と信頼性が低くなる傾向があるためです。

一方、ISNULLは、エンジンにとってintrinsicです。それ以上の基本的なコンポーネントを使用して構築されていません。たとえば、ISNULLの効果をヒストグラムに適用するのは、NULL値のステップを置き換える(必要に応じて圧縮する)のと同じくらい簡単です。スカラー演算子が進むにつれて、それはまだ比較的不透明であり、可能な限り避けるのが最善です。それにもかかわらず、一般的に言えば、CASEベースの代替よりも、オプティマイザーに優しい(オプティマイザーに優しくない)。

CE(70および120+)は、SQL Server標準でさえ、very複雑です。各演算子に(秘密の式を使用した)単純なロジックを適用する場合ではありません。 CEは、キーと機能の依存関係を認識しています。頻度、多変量統計、およびヒストグラムを使用して推定する方法を知っています。そして、特別なケース、改良、チェックとバランス、そしてサポート構造の絶対的なトンがあります。それはしばしば推定します。複数の方法(頻度、ヒストグラム)で結合し、2つの方法の違いに基づいて結果または調整を決定します。

カバーする最後の基本事項:最初のカーディナリティの推定は、クエリツリーのすべての操作に対して、ボトムアップで実行されます。選択性とカーディナリティーは、最初にリーフオペレーター(基本関係)に対して導出されます。変更されたヒストグラムと密度/頻度情報は、親演算子に対して導出されます。ツリーが上に行くほど、エラーが蓄積する傾向があるため、推定の品質が低下する傾向があります。

この単一の初期包括的推定は開始点を提供し、最終的な実行計画が考慮される前に発生します(それは、些細な計画のコンパイル段階でさえある前に発生します)。この時点でのクエリツリーは、クエリの記述形式をかなり厳密に反映する傾向があります(ただし、サブクエリが削除され、簡略化が適用されています)。

SQL Serverは最初の推定の直後にヒューリスティックな結合の並べ替えを実行します。これは、大まかに言って、ツリーを並べ替えて小さなテーブルと高い選択性の結合を最初に配置しようとします。また、外部結合とクロス積の前に内部結合を配置しようとします。その機能は広範囲ではありません。その取り組みは網羅的ではありません。また、物理的コストは考慮されていません(それらはまだ存在していないため、統計情報とメタデータ情報のみが存在します)。ヒューリスティックな並べ替えは、単純な内部等結合ツリーで最も成功します。コストベースの最適化の「より良い」出発点を提供するために存在します。

この結合のカーディナリティの見積もりがなぜそれほど大きいのですか?

MCVEには、「異常な」大部分が冗長なmany-to-many結合、および述語内のCOALESCEとの等価結合があります。演算子ツリーには、内部結合lastもあります。これは、ヒューリスティックな結合の並べ替えで、ツリーをより優先される位置に移動できませんでした。すべてのスカラーと投影を除いて、結合ツリーは次のとおりです。

LogOp_Join [ Card=4.52803e+009 ]
    LogOp_LeftOuterJoin [ Card=481577 ]
        LogOp_LeftOuterJoin [ Card=481577 ]
            LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_LeftOuterJoin [ Card=481577 ]
                LogOp_Get TBL: X_DRIVING_TABLE(alias TBL: dt) [ Card=481577 ]
                LogOp_Get TBL: X_DETAIL_1(alias TBL: d1) [ Card=70 ]
                LogOp_Get TBL: X_DETAIL_LINK(alias TBL: lnk) [ Card=47 ]
            LogOp_Get TBL: X_DETAIL_2(alias TBL: d2) X_DETAIL_2 [ Card=119 ]
        LogOp_Get TBL: X_DETAIL_3(alias TBL: d3) X_DETAIL_3 [ Card=281 ]
    LogOp_Get TBL: X_LAST_TABLE(alias TBL: lst) X_LAST_TABLE [ Card=94025 ]

誤った最終見積もりがすでに行われていることに注意してください。 Card=4.52803e+009として出力され、倍精度浮動小数点値4.5280277425e + 9(10進数では4528027742.5)として内部的に保存されます。

元のクエリの派生テーブルが削除され、投影が正規化されました。初期のカーディナリティと選択性の推定が実行されたツリーのSQL表現は次のとおりです。

SELECT 
    PRIMARY_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)
FROM X_DRIVING_TABLE dt
LEFT OUTER JOIN X_DETAIL_1 d1
    ON dt.ID = d1.ID
LEFT OUTER JOIN X_DETAIL_LINK lnk 
    ON d1.LINK_ID = lnk.LINK_ID
LEFT OUTER JOIN X_DETAIL_2 d2 
    ON dt.ID = d2.ID
LEFT OUTER JOIN X_DETAIL_3 d3 
    ON dt.ID = d3.ID
INNER JOIN X_LAST_TABLE lst 
    ON lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)

(余談ですが、繰り返されるCOALESCEも最終計画に存在します-最終計算スカラーに1回、内部結合の内側に1回)。

最終結合に注目してください。この内部結合は(定義により)X_LAST_TABLEと先行する結合出力のデカルト積であり、lst.JOIN_ID = COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)の選択(結合述語)が適用されます。デカルト積の基数は、単に481577 * 94025 = 45280277425です。

それには、述語の選択性を決定して適用する必要があります。不透明な展開されたCOALESCEツリー(UNIONおよびIIFの観点から、覚えておく)と、重要な情報への影響、派生したヒストグラム、および以前の「異常な」頻度の組み合わせ「ほとんどが冗長な多対多の外部結合を組み合わせたものは、CEが通常の方法で許容可能な見積もりを導き出すことができないことを意味します。

その結果、それは推測ロジックに入ります。推測ロジックはやや複雑で、「教育された」推測と「それほど教育されていない」推測アルゴリズムの層が試されます。推測のより良い根拠が見つからない場合、モデルは最後の手段の推測を使用します。これは、等価比較のために次のようになります。sqllang!x_Selectivity_Equal =固定0.1選択性(10%推測):

Call stack

-- the moment of Doom
movsd xmm0,mmword ptr [sqllang!x_Selectivity_Equal

結果は、前述のように、デカルト積で0.1の選択性になります:481577 * 94025 * 0.1 = 4528027742.5(〜4.52803e + 009)。

書き換え

問題のある結合がコメントアウトの場合、固定選択性の「最後の手段の推測」が回避されるため、より適切な推定が生成されます(キー情報は、 1-M結合)。 COALESCE結合述語はCEに適していないため、推定の品質は依然として信頼度が低くなっています。改訂された見積もりは、少なくとも見た目を人間にとってより妥当なものにしていると思います。

クエリがX_DETAIL_LINKへの外部結合が最後に配置されて記述されている場合、ヒューリスティックリオーダはそれをX_LAST_TABLEへの最後の内部結合と交換できます。内部結合をproblem外部結合のすぐ隣に置くと、ほとんどの効果が-冗長な「異常な」多対多の外部結合が後にCOALESCEのトリッキーな選択性推定。繰り返しになりますが、推定値は固定された推測より少し優れており、おそらく法廷での決定的な反対尋問に耐えられないでしょう。

内部結合と外部結合の混合の並べ替えは困難で時間がかかります(ステージ2の完全最適化でさえも、理論的な動きの限定されたサブセットのみが試行されます)。

Max Vernonの回答で提案されているネストされたISNULLは、救済の固定推測を回避することに成功していますが、最終的な推定値はありそうもないゼロ行です(良識のために1行に引き上げられています)。これは、計算に含まれるすべての統計的根拠についても、1行の固定推測である可能性があります。

結合カーディナリティの推定値は0〜481577行になると思います。

これは、カーディナリティの推定が物理的に異なるが論理的および意味的に同一のサブツリーで異なる時間に(コストベースの最適化中に)発生する可能性があることを受け入れたとしても、合理的な期待です。最高(メモグループごと)。プラン全体の一貫性が保証されていないということは、個々の結合が敬意を払うべきであるという意味ではありません。

一方、最後の手段の推測に終わった場合、希望はすでに失われているので、なぜわざわざ。私たちは知っているすべてのトリックを試し、あきらめました。他に何もない場合、ワイルドな最終見積もりは、このクエリのコンパイルと最適化中にCE内ですべてがうまくいったわけではないという素晴らしい警告サインです。

MCVEを試したところ、120以上のCEは、元のクエリに対してゼロ(= 1)行の最終的な見積もり(ネストされたISNULLなど)を生成しましたが、これは私の考え方には受け入れられません。

実際の解決策はおそらくCOALESCEまたはISNULLなしの単純な等結合を可能にするための設計変更と、理想的には外部キーとクエリのコンパイルに役立つその他の制約を含みます。

15
Paul White 9

COALESCE(d1.JOIN_ID, d2.JOIN_ID, d3.JOIN_ID)が_Compute Scalar_に結合された結果の_X_LAST_TABLE.JOIN_ID_演算子が問題の根本的な原因であると思います。歴史的に、計算スカラーは正確にコストを計算することが困難でした12

正確な統計で最小限の完全な検証可能な例(ありがとう!)を提供したので、CASEが拡張されたCOALESCE機能が結合に不要になるようにクエリを書き直すことができます、行の推定がはるかに正確になります。 明らかにより正確な全体的な原価計算末尾の補遺を参照してください。

_SELECT COALESCE(dm.d1ID, dm.d2ID, dm.d3ID)
FROM
(
    SELECT d1ID = d1.JOIN_ID
        , d2ID = d2.JOIN_ID
        , d3ID = d3.JOIN_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst 
    ON (dm.d1ID IS NOT NULL AND dm.d1ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NOT NULL AND dm.d2ID = lst.JOIN_ID)
    OR (dm.d1ID IS NULL AND dm.d2ID IS NULL AND dm.d3ID IS NOT NULL AND dm.d3ID = lst.JOIN_ID);
_

_xID IS NOT NULL_は技術的には必要ありませんが、_ID = JOIN_ID_はnull値で結合しないため、意図をより明確に表すためにそれらを含めました。

プラン1 および プラン2

計画1:

enter image description here

計画2:

enter image description here

新しいクエリには、並列化のメリットがあります。また、新しいクエリの推定出力行数は1で、1日の終わりには、元のクエリの推定値4528030000よりも悪い場合があります。新しいクエリの選択演算子のサブツリーコストは243210にありますが、元のクロックは536.535にあり、これは明らかに少ないです。そうは言っても、最初の見積もりが現実に近いとは思えません。


補遺1。

@ -Lamakとのディスカッションによって The Heap™ についてさまざまな人々とさらに協議した後、上記の私の観察クエリは、並列処理であっても、ひどく実行されているようです。優れたパフォーマンスの両方を可能にするソリューションは、COALESCE(x,y,z)ISNULL(ISNULL(x, y), z)で置き換えることで構成されます。のように:

_SELECT dm.PRIMARY_ID
FROM
(
    SELECT ISNULL(ISNULL(d1.JOIN_ID, d2.JOIN_ID), d3.JOIN_ID) PRIMARY_ID
    FROM X_DRIVING_TABLE dt
    LEFT OUTER JOIN X_DETAIL_1 d1 ON dt.ID = d1.ID
    LEFT OUTER JOIN X_DETAIL_LINK lnk ON d1.LINK_ID = lnk.LINK_ID
    LEFT OUTER JOIN X_DETAIL_2 d2 ON dt.ID = d2.ID
    LEFT OUTER JOIN X_DETAIL_3 d3 ON dt.ID = d3.ID
) dm
INNER JOIN X_LAST_TABLE lst ON dm.PRIMARY_ID = lst.JOIN_ID;
_

COALESCEは、クエリオプティマイザーによって「内部」でCASEステートメントに変換されます。そのため、カーディナリティエスティメータは、COALESCE内に埋め込まれた列の信頼性の高い統計を発見することが困難になります。 ISNULLが組み込み関数であることは、カーディナリティエスティメータにとってはるかに「オープン」です。ターゲットがnull可能でないことがわかっている場合、ISNULLを最適化して削除できることも何の価値もありません。

ISNULLバリアントの計画は次のようになります。

enter image description here

(計画バージョンを貼り付け ここ )。

参考までに、私は上記のグラフィカルな計画を作成するために使用した偉大なプランエクスプローラーのために Sentry One を支持します。

10
Max Vernon