web-dev-qa-db-ja.com

SQL ServerがUNIONを最適化しないのはなぜですか?

次のクエリを検討してください( SQL Fiddle ):

クエリ1:

SELECT * INTO #TMP1 FROM Foo
UNION
SELECT * FROM Boo
UNION
SELECT * FROM Koo;

クエリ2:

SELECT * INTO #TMP2 FROM Foo
UNION
SELECT * FROM Boo
UNION ALL
SELECT * FROM Koo;

KooはBoo/Fooと重複しないため、最終結果は同じです。問題は、最初のUNION/UNIONの組み合わせが単一のSORT操作にマージされないのはなぜですか?

7
孔夫子

クエリオプティマイザーにはn項演算子がありますが、実行エンジンはかなり少なくなっています。説明のために、テーブルの簡略版を使用します (SQL Fiddle)

SELECT DISTINCT
    number
INTO foo
FROM master..spt_values
WHERE 
    number < 1000;

SELECT DISTINCT
    number
INTO boo
FROM master..spt_values
WHERE 
    number between 300 and 1005;

SELECT DISTINCT
    number
INTO koo
FROM master..spt_values
WHERE 
    number > 1006;

ALTER TABLE dbo.foo ADD PRIMARY KEY (number);
ALTER TABLE dbo.boo ADD PRIMARY KEY (number);
ALTER TABLE dbo.koo ADD PRIMARY KEY (number);

これらのテーブルとデータを前提として、3方向のUNIONクエリの入力ツリーを見てみましょう。

SELECT f.number FROM dbo.foo AS f
UNION
SELECT b.number FROM dbo.boo AS b
UNION
SELECT k.number FROM dbo.koo AS k
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8605);

LogOp_Union
    OUTPUT(COL: Union1006 )
    CHILD(QCOL: [f].number)
    CHILD(QCOL: [b].number)
    CHILD(QCOL: [k].number)
    LogOp_Project
        LogOp_Get TBL: dbo.foo(alias TBL: f)
        AncOp_PrjList 
    LogOp_Project
        LogOp_Get TBL: dbo.boo(alias TBL: b)
        AncOp_PrjList 
    LogOp_Project
        LogOp_Get TBL: dbo.koo(alias TBL: k)
        AncOp_PrjList 

論理和演算子には、1つの出力と3つの子入力があります。コストベースの最適化の後、選択された物理ツリーは3つの入力を持つマージユニオンです。

SELECT f.number FROM dbo.foo AS f
UNION
SELECT b.number FROM dbo.boo AS b
UNION
SELECT k.number FROM dbo.koo AS k
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8607);

PhyOp_MergeUnion
    PhyOp_Range TBL: dbo.foo(alias TBL: f)(1) ASC
    PhyOp_Range TBL: dbo.boo(alias TBL: b)(1) ASC
    PhyOp_Range TBL: dbo.koo(alias TBL: k)(1) ASC

オプティマイザの出力は、実行エンジン(n-aryマージユニオンなし)が処理できる形式に再加工されます。

Merge union plan

最適化後の書き換えにより、n項PhyOp_MergeUnionが複数のマージユニオン演算子に展開されます。見積もられたすべてのコストが「元の」ユニオン演算子に関連付けられたままになっていることに注意してください。その他の見積りはゼロです。

オプティマイザがn項演算子を使用した共用体について理由とすることは、最初の例を2番目の例と同じ計画に書き直さない理由の1つの説明を提供します(3者共用体は単一のツリーノードです)。

2番目の理由は、「オーバーラップの欠如」を強制する制約がないことです。制約が設定される前は、bookooの和集合が重複を生成しないことが保証されていないため、重複削除計画(この場合はマージユニオン)を取得します。

SELECT b.number FROM dbo.boo AS b
UNION
SELECT k.number FROM dbo.koo AS k;

boo/koo without constraints

次の制約を追加すると、クエリのキャッシュされたプランを無効にしない限り、非重複条件に違反することはありません。

ALTER TABLE dbo.foo WITH CHECK ADD CHECK (number < 1000);
ALTER TABLE dbo.boo WITH CHECK ADD CHECK (number BETWEEN 300 AND 1005);
ALTER TABLE dbo.koo WITH CHECK ADD CHECK (number > 1006);

これで、オプティマイザが単純に連結しても安全です。

boo/koo with constraints

ただし、これらの制約が設定されている場合でも、オプティマイザは通常、n項演算子を分割して代替案を探索することを考慮していないため、3ウェイユニオンクエリは3つのユニオンとして表示されます。 n項演算子は、検索スペースを制御し続けるのに非常に役立ちます。最適なプランをすばやく見つけるというオプティマイザの目標を考えると、それを分解すると逆効果になることがよくあります。

SELECT f.number FROM dbo.foo AS f
UNION
SELECT b.number FROM dbo.boo AS b
UNION
SELECT k.number FROM dbo.koo AS k;

Merge union plan with constraints

UNIONおよびUNION ALLとして記述した場合、n項演算子は使用できなくなり(型が一致しなくなり)、ツリーに個別のノードができます。

SELECT f.number FROM dbo.foo AS f
UNION
SELECT b.number FROM dbo.boo AS b
UNION ALL
SELECT k.number FROM dbo.koo AS k
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8605);

LogOp_UnionAll
    OUTPUT(COL: Union1007 )
    CHILD(COL: Union1004 )
    CHILD(QCOL: [k].number)

    LogOp_Union
        OUTPUT(COL: Union1004 )
        CHILD(QCOL: [f].number)
        CHILD(QCOL: [b].number)

        LogOp_Project
            LogOp_Get TBL: dbo.foo(alias TBL: f)
            AncOp_PrjList 

        LogOp_Project
            LogOp_Get TBL: dbo.boo(alias TBL: b)
            AncOp_PrjList 

    LogOp_Project
        LogOp_Get TBL: dbo.koo(alias TBL: k)
        AncOp_PrjList 
18
Paul White 9

SQL Server doesには3方向のセット操作があります。 CONCATENATION演算子はn入力を受け入れます。たとえば、10個のテーブルがあるとします。

CREATE TABLE Test01 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80)); 
CREATE TABLE Test02 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test03 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test04 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test05 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test06 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test07 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test08 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test09 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));
CREATE TABLE Test10 (SomeKey INTEGER NOT NULL, SomeAttribute VARCHAR(80));

そして、すべてを結合して同じキーを持つ各テーブルの行を見つけるクエリ:

SELECT * FROM
(
SELECT * FROM Test01 UNION ALL
SELECT * FROM Test02 UNION ALL
SELECT * FROM Test03 UNION ALL
SELECT * FROM Test04 UNION ALL
SELECT * FROM Test05 UNION ALL
SELECT * FROM Test06 UNION ALL
SELECT * FROM Test07 UNION ALL
SELECT * FROM Test08 UNION ALL
SELECT * FROM Test09 UNION ALL
SELECT * FROM Test10
) AS Bunch
WHERE SomeKey = 39;

(TABLE SCAN演算子で述語Push downを使用して)一致する行を取得し、すべての結果をSELECT演算子に連結するクエリプランが表示されます。

プランがマージされてから並べ替えられないのは、処理が非常に遅くなるためであり、UNION操作を実装するために並べ替えは必要ありません。 BOO、FOO、およびKOOテーブルで、主キーを宣言しました。 CLUSTERED INDEX SCANアクセサーが行を列挙すると、それらは基になるクラスター化インデックスの順序で生成されます-保証されています。 2つのセットを連結して結果をソートすることは、MERGE JOIN演算子を使用するよりもはるかに遅く、両方のセットがソートおよびインデックス付けされるため、MJ演算子を非常に簡単に使用できます。

3
MikeB