クエリを変更してオペレーターの見積もりを改善する


14

許容時間内に実行されるクエリがありますが、そこから可能な限り最高のパフォーマンスを絞り出したいです。

私が改善しようとしている操作は、ノード17からのプランの右側にある「インデックスシーク」です。

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

適切なインデックスを追加しましたが、その操作に対して得られる推定値は、想定される値の半分です。

インデックスを変更し、一時テーブルを追加してクエリを書き直すことを探しましたが、適切な見積もりを得るためにこれ以上単純化することはできませんでした。

他に私が試すことができるものについて誰か提案がありますか?

完全な計画とその詳細については、こちらをご覧ください

匿名化されていないプランはここにあります。

更新:

質問の最初のバージョンは多くの混乱を引き起こしたと感じているので、いくつかの説明とともに元のコードを追加します。

create procedure [dbo].[someProcedure] @asType int, @customAttrValIds idlist readonly
as
begin
    set nocount on;

    declare @dist_ca_id int;

    select *
    into #temp
    from @customAttrValIds
        where id is not null;

    select @dist_ca_id = count(distinct CustomAttrID) 
    from CustomAttributeValues c
        inner join #temp a on c.Id = a.id;

    select a.Id
        , a.AssortmentId 
    from Assortments a
        inner join AssortmentCustomAttributeValues acav
            on a.Id = acav.Assortment_Id
        inner join CustomAttributeValues cav 
            on cav.Id = acav.CustomAttributeValue_Id
    where a.AssortmentType = @asType
        and acav.CustomAttributeValue_Id in (select id from #temp)
    group by a.AssortmentId
        , a.Id
    having count(distinct cav.CustomAttrID) = @dist_ca_id
    option(recompile);

end

回答:

  1. pasteThePlanリンクの最初の名前が奇妙なのはなぜですか?

    回答:SQL Sentry Plan Explorerの匿名化プランを使用したためです。

  2. なんでOPTION RECOMPILE

    回答:パラメータスニッフィングを回避するために再コンパイルする余裕があるためです(データが歪んでいる/歪んでいる可能性があります)。私はテストしましたが、オプティマイザーが使用中に生成するプランに満足していOPTION RECOMPILEます。

  3. WITH SCHEMABINDING

    回答:私は本当にそれを避けたいので、インデックス付きビューがある場合にのみ使用します。とにかく、これはシステム関数(COUNT())なので、SCHEMABINDINGここでは使いません。

考えられるその他の質問への回答:

  1. なぜ使用するのINSERT INTO #temp FROM @customAttrributeValuesですか?

    回答:クエリにプラグインされた変数を使用する場合、変数の操作から生じる推定値は常に1であることに気づき、今ではわかっているため、データを一時テーブルに入れてテストし、推定値実際の行と等しいことをテストしました。

  2. なぜ使用したのand acav.CustomAttributeValue_Id in (select id from #temp)ですか?

    回答:#tempでJOINに置き換えることもできましたが、開発者は非常に混乱し、INオプションを提供しました。交換してもどちらにせよ違いがあるとは思いませんが、これで問題はありません。


#temp作成と使用はパフォーマンスの問題であり、ゲインの問題ではないと思います。インデックス化されていないテーブルに保存して、一度だけ使用するようにします。完全に削除してみてください(そしておそらくそれin (select id from #temp)existsサブクエリに変更してください。
ypercubeᵀᴹ17年

@ypercubeᵀᴹ確かに、一時テーブルの代わりに変数を使用して読み取ったページがわずかに少なくなります。
ラドゥゲオルギウ

ちなみに、テーブル変数は、オプション(再コンパイル)で使用すると正しい行数の見積もりを提供しますが、詳細な統計情報、カーディナリティなどはまだありません。
TH

@THさて、select id from @customAttrValIds代わりにを使用し、変数と#temp(実際select id from #tempの行数に一致した)の行数の推定値を使用した場合、実際の実行計画を推定値で調べました。だから私はに置き換えました。そして、I DOは、彼らがTBL変数を使用した場合、そのための推定値は常に1と、彼らは一時テーブルを使用することになり、より良い推定値を得るための改善としてになるという(ブレントOまたはアーロン・ベルトランから)話を覚えています。13@#
ラドゥゲオルギウ

@RaduGheorghiuええ、でも彼らの世界では、オプション(再コンパイル)がオプションになることはめったになく、他の正当な理由で一時テーブルを好むこともあります。ここに見られるように、それは計画を変更ないようたぶん推定1のように、単にいつも間違っショー、:theboreddba.com/Categories/FunWithFlags/...
TH

回答:


12

この計画は、SQL Server 2008 R2 RTMインスタンス(ビルド10.50.1600)でコンパイルされました。Service Pack 3(ビルド10.50.6000)をインストールしてから、最新のパッチをインストールして、最新の(最新の)ビルド10.50.6542にアップグレードする必要があります。これは、セキュリティ、バグ修正、新機能など、さまざまな理由で重要です。

パラメータ埋め込み最適化

現在の質問に関連して、SQL Server 2008 R2 RTMはのParameter Embedding Optimization(PEO)をサポートしていませんでしたOPTION (RECOMPILE)。現時点では、主な利点の1つを認識せずに再コンパイルのコストを支払っています。

PEOが利用可能な場合、SQL Serverは、クエリプランでローカル変数およびパラメーターに直接格納されているリテラル値を使用できます。これにより、劇的な簡素化とパフォーマンスの向上につながります。それについての詳細は、私の記事「パラメータースニッフィング、埋め込み、およびRECOMPILEオプション」にあります。

流出、ハッシュ、ソート、交換

これらは、クエリがSQL Server 2012以降でコンパイルされたときにのみ実行プランに表示されます。以前のバージョンでは、プロファイラーまたは拡張イベントを使用してクエリの実行中に流出を監視する必要がありました。こぼれは常に、tempdbをバッキングする永続ストレージへの(およびからの)物理I / Oをもたらします。これは、特にこぼれが大きい場合、またはI / Oパスに圧力がかかっている場合、パフォーマンスに重要な結果をもたらします。

実行計画には、2つのハッシュマッチ(集計)演算子があります。ハッシュテーブル用に予約されているメモリは、出力行推定に基づいています(つまり、実行時に見つかったグループの数に比例します)。許可されたメモリは、実行が開始される直前に固定され、インスタンスの空きメモリ量に関係なく、実行中に拡大することはできません。提供されたプランでは、両方のHash Match(Aggregate)演算子がオプティマイザーが予想するよりも多くの行を生成するため、実行時にtempdbへの流出が発生する可能性があります。

計画にはハッシュ一致(内部結合)演算子もあります。ハッシュテーブル用に予約されているメモリは、プローブ側の入力行の推定に基づいています。プローブ入力は847,399行を推定しますが、実行時に1,223,636が検出されます。この過剰はハッシュ流出を引き起こす可能性もあります。

冗長集約

ノード8のハッシュ一致(集合)はでグループ化操作を実行します(Assortment_Id, CustomAttrID)が、入力行は出力行と等しくなります。

ノード8ハッシュ一致(集合)

これは、列の組み合わせがキーであることを示唆しています(したがって、グループ化は意味的に不要です)。冗長な集計を実行するコストは、ハッシュ分割交換(両側の並列処理演算子)で140万行を2回渡す必要があるため増加します。

関係する列が異なるテーブルからのものであることを考えると、この一意性情報をオプティマイザーに伝えることは通常よりも難しいため、冗長なグループ化操作と不要な交換を回避できます。

非効率的なスレッド分散

Joe Obbishの回答で述べたように、ノード14での交換はハッシュ分割を使用してスレッド間で行を分散します。残念ながら、行数が少なく、使用可能なスケジューラーがあるため、3行すべてが1つのスレッドになります。明らかに並列なプランは、ノード9での交換まで直列に(並列オーバーヘッドで)実行されます。

ノード13で個別ソートを削除することで、これに対処できます(ラウンドロビンまたはブロードキャストパーティショニングを取得する)。これを行う最も簡単な方法は、#tempテーブルにクラスター化プライマリキーを作成し、テーブルを読み込むときに個別の操作を実行することです。

CREATE TABLE #Temp
(
    id integer NOT NULL PRIMARY KEY CLUSTERED
);

INSERT #Temp
(
    id
)
SELECT DISTINCT
    CAV.id
FROM @customAttrValIds AS CAV
WHERE
    CAV.id IS NOT NULL;

一時テーブル統計キャッシュ

の使用にもかかわらずOPTION (RECOMPILE)、SQL Serverは、プロシージャコール間で一時テーブルオブジェクトとそれに関連する統計をキャッシュできます。これは一般的に歓迎されるパフォーマンスの最適化ですが、隣接するプロシージャコールで一時テーブルに同量のデータが入力される場合、再コンパイルされたプランは誤った統計(以前の実行からキャッシュされた)に基づく場合があります。これについては、私の記事「ストアドプロシージャの一時テーブル」と「一時テーブルのキャッシュの説明」で詳しく説明しています。

これを回避OPTION (RECOMPILE)するにUPDATE STATISTICS #TempTableは、一時テーブルにデータが入力された後、クエリで参照される前に、明示的に使用します。

クエリの書き換え

この部分では、#Tempテーブルの作成に対する変更が既に行われていることを前提としています。

ハッシュ流出の可能性と冗長集約(および周囲の交換)のコストを考えると、ノード10でセットを具体化するために費用が発生する可能性があります。

CREATE TABLE #Temp2
(
    CustomAttrID integer NOT NULL,
    Assortment_Id integer NOT NULL,
);

INSERT #Temp2
(
    Assortment_Id,
    CustomAttrID
)
SELECT
    ACAV.Assortment_Id,
    CAV.CustomAttrID
FROM #temp AS T
JOIN dbo.CustomAttributeValues AS CAV
    ON CAV.Id = T.id
JOIN dbo.AssortmentCustomAttributeValues AS ACAV
    ON T.id = ACAV.CustomAttributeValue_Id;

ALTER TABLE #Temp2
ADD CONSTRAINT PK_#Temp2_Assortment_Id_CustomAttrID
PRIMARY KEY CLUSTERED (Assortment_Id, CustomAttrID);

これPRIMARY KEYは、インデックスビルドに正確なカーディナリティ情報が含まれるようにし、一時テーブル統計のキャッシュの問題を回避するために、別の手順で追加されます。

インスタンスに使用可能なメモリが十分にある場合、この実体化はメモリで発生する可能性が非常に高くなります(tempdb I / O を回避)。これは、Eager Writeの動作改善されたSQL Server 2012(SP1 CU10 / SP2 CU1以降)にアップグレードするとさらに起こりやすくなります。

このアクションにより、オプティマイザーは中間セットの正確なカーディナリティー情報を取得し、統計を作成(Assortment_Id, CustomAttrID)し、キーとして宣言できるようになります。

の母集団の計画は次の#Temp2ようになります(のクラスタ化インデックススキャン#Temp、Distinct Sortなし、および交換はラウンドロビン行パーティション化を使用することに注意してください):

#Temp2人口

そのセットが利用可能になると、最終的なクエリは次のようになります。

SELECT
    A.Id,
    A.AssortmentId
FROM
(
    SELECT
        T.Assortment_Id
    FROM #Temp2 AS T
    GROUP BY
        T.Assortment_Id
    HAVING
        COUNT_BIG(DISTINCT T.CustomAttrID) = @dist_ca_id
) AS DT
JOIN dbo.Assortments AS A
    ON A.Id = DT.Assortment_Id
WHERE
    A.AssortmentType = @asType
OPTION (RECOMPILE);

COUNT_BIG(DISTINCT...単純なとして手動で書き換えることもできCOUNT_BIG(*)ますが、新しいキー情報を使用して、オプティマイザーがそれを行います。

最終計画

最終的な計画では、アクセスできないデータに関する統計情報に応じて、ループ/ハッシュ/マージ結合を使用できます。もう1つの小さなメモ:のようなインデックスがCREATE [UNIQUE?] NONCLUSTERED INDEX IX_ ON dbo.Assortments (AssortmentType, Id, AssortmentId);存在すると仮定しました。

とにかく、最終計画で重要なことは、推定値がはるかに優れていることであり、グループ化操作の複雑なシーケンスが単一のStream Aggregateに削減されていることです(メモリを必要としないため、ディスクに流出できません)。

この場合、追加の一時テーブルを使用することで実際にパフォーマンスが向上すると言うのは困難ですが、見積もりと計画の選択は、データ量と分布の経時変化に対する回復力がはるかに高くなります。これは、今日のわずかなパフォーマンス向上よりも、長期的にはより価値があるかもしれません。いずれにせよ、最終的な決定の根拠となる情報がはるかに多くなりました。


9

クエリでのカーディナリティの見積もりは、実際には非常に優れています。特に多くの結合がある場合、実際の行の数と正確に一致する推定行の数を取得することはまれです。結合カーディナリティーの見積もりは、オプティマイザーが正しく動作するために注意が必要です。注意すべき重要な点の1つは、ネストされたループの内側部分の推定行の数は、そのループの実行ごとです。したがって、SQL Serverが463869行がインデックス付きでフェッチされると言った場合、この場合の実際の推定値は実行数(2)* 463869 = 927738であり、実際の行数1391608からそれほど離れていません。推定された行の数は、ノードID 10でネストされたループ結合の直後にほぼ完璧です。

クエリオプティマイザが間違ったプランを選択した場合、またはプランに十分なメモリを許可していない場合、カーディナリティの推定値が低いことがほとんどの問題です。この計画でtempdbに流出が見られないので、メモリは大丈夫に見えます。呼び出すネストされたループ結合には、小さな外部テーブルとインデックス付き内部テーブルがあります。それのどこが悪いんだい?正確に言うと、クエリオプティマイザーはここでどのように異なることを期待しますか?

パフォーマンスの向上に関して、私が際立っているのは、SQL Serverがハッシュアルゴリズムを使用して、すべての行が同じスレッド上にある並列行を分散していることです。

糸のアンバランス

その結果、1つのスレッドがインデックスシークのすべての作業を実行します。

スレッド不均衡シーク

つまり、ノードID 9で再パーティションストリーム演算子が実行されるまで、クエリは事実上並列に実行されないことを意味します。これにより、2つのスレッドがノードID 17のインデックスシークを実行TOPできます。余分な演算子を追加すると、ラウンドロビンパーティション分割が行われる場合があります。必要に応じてここに詳細を追加できます。

カーディナリティーの見積もりに集中したい場合は、最初の結合の後に行を一時テーブルに入れることができます。一時テーブルの統計を収集して、オプティマイザに、呼び出したネストされたループ結合の外部テーブルに関する詳細情報を提供する場合。また、ラウンドロビンパーティション化につながる可能性があります。

トレースフラグ4199または2301を使用していない場合は、それらを検討できます。トレースフラグ4199はさまざまなオプティマイザーの修正を提供しますが、一部のワークロードが低下する可能性があります。トレースフラグ2301は、クエリオプティマイザーの結合カーディナリティの仮定の一部を変更し、それをより困難にします。どちらの場合も、有効にする前に慎重にテストしてください。


-2

1.4ミルがハッシュまたはマージ結合を使用したインデックス(クラスターではなく)スキャンを選択するのに十分なテーブルの部分でない限り、その結合のより良い推定値を取得してもプランは変更されないと思います。ここではそうではなく、実際は役に立たないと思いますが、CustomAttributeValuesに対するinner joinをinner hash joinおよびinner merge joinに置き換えることで効果をテストできます。

また、コードをより広く見てきたので、それを改善する方法がわかりません。もちろん、間違っていることが証明されることに興味があります。そして、あなたが達成しようとしていることの完全な論理を投稿したいと思うなら、私は別の外観に興味があります。


3
そのクエリには非常に大きなプランのスペースがあり、結合順序とネスト、並列処理、ローカル/グローバル集約などの多くのオプションがあります。それらのほとんどは、派生統計(生のカーディナリティと同様に)の変更の影響を受けます。また、プランヒント10で、ヒントがサイレントOPTION(FORCE ORDER)で提供されるため、一般に結合ヒントを避ける必要があることに注意してください。これにより、オプティマイザはテキストシーケンスからの結合の並べ替えを防ぎます。
ポールホワイトモニカーを復活

-12

[クラスタ化されていない]インデックスシークから改善するつもりはありません。非クラスター化インデックスシークよりも優れている唯一のものは、クラスター化インデックスシークです。

また、私は過去10年間SQL DBAであり、その5年前にSQL開発者でしたが、私の経験では、実行プランを調べてSQLクエリの改善を見つけることは非常にまれです」他の方法で見つける。実行計画を生成する主な理由は、多くの場合、パフォーマンスの向上のために追加できるインデックスの欠落を提案するためです。

主なパフォーマンスの向上は、非効率性がある場合、SQLクエリ自体を調整することです。たとえば、数か月前、SELECT UNION SELECTスタイルピボットテーブルを書き換えて標準のSQL PIVOT演算子を使用することにより、160倍高速に実行するSQL関数を取得しました。

insert into Variable1 values (?), (?), (?)


select *
    into Object1
    from Variable2
        where Column1 is not null;



select Variable3 = Function1(distinct Column2) 
    from Object2 Object3
        inner join Object1 Object4 on Object3.Column1 = Object4.Column1;



select Object4.Column1
        , Object4.Column3 
    from Object5 Object4
        inner join Object6 Object7
            on Object4.Column1 = Object7.Column4
        inner join Object2 Object8 
            on Object8.Column1 = Object7.Column5
    where Object4.Column6 = Variable4
        and Object7.Column5 in (select Column1 from Object1)
    group by Object4.Column3
        , Object4.Column1
    having Function1(distinct Object8.Column2) = Variable3
    option(recompile);

それでは、SELECT * INTO一般的に標準よりも効率が悪いことを見てみましょうINSERT Object1 (column list) SELECT column list。だから私はそれを書き直します。次に、Function1がなしで定義されている場合WITH SCHEMABINDINGWITH SCHEMABINDING句を追加すると、より高速に実行できるようになります。

Object2をObject3としてエイリアスするなど、意味のないエイリアスを多数選択しました。コードを難読化しないより適切なエイリアスを選択する必要があります。「Object7.Column5 in(Object1からColumn1を選択)」があります。

INこの性質の節は常により効率的に書かれていEXISTS (SELECT 1 FROM Object1 o1 WHERE o1.Column1 = Object7.Column5)ます。おそらく私はそれを他の方法で書くべきだった。EXISTSは常に少なくともと同等になりますIN。常に良いとは限りませんが、通常はそうです。

また、option(recompile)ここでクエリのパフォーマンスが向上しているとは思いません。私はそれを削除することをテストします。


6
非クラスター化インデックスのシークがクエリをカバーしている場合、クラスター化インデックスのシークよりもほぼ常に優れています。定義により、クラスター化インデックスにはすべての列があり、非クラスター化インデックスの列が少ないため、ページシークが少なくて済みます(そしてデータを取得するためのbツリーへのステップのレベルを減らします。したがって、クラスター化インデックスシークが常に優れていると言うのは正確ではありません。
エリック
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.