ユーザー定義関数の最適化の問題


26

1行のみをフェッチする必要があるにもかかわらず、SQLサーバーがテーブル内のすべての値に対してユーザー定義関数を呼び出すことを決定する理由を理解するのに問題があります。実際のSQLはもっと複雑ですが、問題をこれまで減らすことができました。

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

このクエリでは、SQL Serverは、ORDERLINEから返される推定行数と実際の行数が1(主キー)であっても、PRODUCTテーブルに存在するすべての値に対してGetGroupCode関数を呼び出すことを決定します。

クエリプラン

行カウントを示すプランエクスプローラーの同じプラン:

計画エクスプローラー テーブル:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

スキャンに使用されているインデックスは次のとおりです。

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

この関数は実際には少し複雑ですが、次のようなダミーのマルチステートメント関数でも同じことが起こります。

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

SQLサーバーにトップ1の製品を強制的にフェッチさせることにより、パフォーマンスを「修正」することができました。

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

それから、計画の形状も、元々予想されていたものに変わります。

トップのクエリプラン

また、インデックスPRODUCT_FACTORYがクラスター化インデックスPRODUCT_PKよりも小さいと影響がありますが、クエリでPRODUCT_PKを使用するように強制しても、計画は元のプランと同じで、関数を6655呼び出します。

ORDERHDRを完全に省くと、プランはまずORDERLINEとPRODUCTの間のネストされたループから始まり、関数は1回だけ呼び出されます。

すべての操作は主キーを使用して行われるため、これが原因である理由と、これを簡単に解決できないより複雑なクエリで発生した場合の修正方法を理解したいと思います。

編集:テーブルステートメントを作成します。

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

回答:


30

計画を立てるには、主に3つの技術的な理由があります。

  1. オプティマイザーのコスト計算フレームワークは、非インライン関数を実際にサポートしていません。関数定義の内部を調べてどれくらいの費用がかかるかを確認することはせず、非常に小さな固定コストを割り当てるだけで、関数が呼び出されるたびに1行の出力が生成されると推定します。これらの両方のモデリングの仮定は、非常に多くの場合完全に安全ではありません。新しいカーディナリティ推定器を有効にすると、2014年に状況がわずかに改善されます。これは、固定1行の推測が固定の100行の推測に置き換えられたためです。ただし、非インライン関数のコンテンツのコスト計算はまだサポートされていません。
  2. SQL Serverは最初に結合を折りたたみ、単一の内部n項論理結合に適用します。これは、オプティマイザが後で結合順序について判断するのに役立ちます。単一のn項結合を候補結合順序に拡張する方法は後ほど説明しますが、これは主にヒューリスティックに基づいています。たとえば、内部結合は外部結合の前にあり、小さなテーブルと選択的結合は大きなテーブルと選択的でない結合の前にあります。
  3. SQL Serverは、コストベースの最適化を実行するときに、労力をオプションのフェーズに分割して、低コストのクエリの最適化に時間がかかりすぎる可能性を最小限に抑えます。検索0、検索1、および検索2の3つの主要なフェーズがあります。各フェーズにはエントリ条件があり、後のフェーズでは、前のフェーズよりも多くのオプティマイザ探索が可能になります。クエリは、最小能力の検索フェーズであるフェーズ0の対象となります。十分な低コストのプランが見つかり、後のステージは入力されません。

UDFに割り当てられたカーディナリティの推定値が小さいため、n項結合拡張ヒューリスティックは、残念ながら、ツリー内で希望よりも早く再配置します。

クエリは、少なくとも3つの結合(適用を含む)があるため、検索0の最適化にも適しています。奇妙なスキャンで得られる最終的な物理計画は、ヒューリスティックに推定された結合順序に基づいています。コストが十分に低いため、オプティマイザはプランを「十分」と見なします。UDFの低コストの見積もりとカーディナリティは、この早期終了に貢献します。

検索0(トランザクション処理フェーズとも呼ばれます)は、カーディナリティの低いOLTPタイプのクエリを対象とし、通常、ネストループ結合を特徴とする最終計画を使用します。さらに重要なこととして、検索0は、オプティマイザーの探索機能の比較的小さなサブセットのみを実行します。このサブセットには、結合(ルールPullApplyOverJoin)を介したクエリツリーの適用のプルアップは含まれません。これは、テストケースで、結合の上にUDF適用を再配置し、操作のシーケンスの最後に表示されるように(必要に応じて)必要なものです。

また、オプティマイザーが、単純なネストされたループ結合(結合自体の結合述語)と相関インデックス付き結合(適用)を決定できる問題もあります。後者は通常、望ましい平面形状ですが、オプティマイザーは両方を調査できます。不正確なコストとカーディナリティの見積もりにより、提出された計画(スキャンの説明)のように、適用されないNL結合を選択できます。

そのため、通常は過剰なリソースを使用せずに短期間で適切な計画を見つけるのに適切に機能するいくつかの一般的なオプティマイザー機能に関係する相互作用の理由が複数あります。空のテーブルであっても、サンプルクエリの「予想される」計画形状を作成するには、いずれかの理由を回避するだけで十分です。

検索0を無効にして空のテーブルを計画する

検索0プランの選択、オプティマイザーの早期終了を回避したり、UDFのコストを改善したりするためのサポートされた方法はありません(このためのSQL Server 2014 CEモデルの制限された拡張は別として)。これにより、プランガイド、手動クエリの書き換え(TOP (1)アイデアや中間一時テーブルの使用など)、非インライン関数のような低コストの「ブラックボックス」(QOの観点から)が回避されます。

書き換えCROSS APPLYとしてOUTER APPLY例えば任意の拒否(それは現在、早期参加崩壊作業の一部を防止していますが、元のクエリのセマンティクスを保護するために、注意しなければならないよう、また、仕事をすることができNULL、Aに背を崩壊オプティマイザせずに、導入される可能性があります-extended行をクロス適用)。ただし、この動作が安定していることは保証されていないことに注意する必要があります。そのため、SQL Serverにパッチを適用するかアップグレードするたびに、このような動作を再テストする必要があります。

全体として、適切な解決策は、判断できないさまざまな要因に依存します。ただし、将来的には常に機能することが保証されているソリューションを検討することをお勧めします。可能なソリューションは、オプティマイザーに対して(反対ではなく)動作します。


24

これは、オプティマイザによるコストベースの決定であるように見えますが、かなり悪い決定です。

50000行をPRODUCTに追加すると、オプティマイザーはスキャンが多すぎると判断し、3回のシークと1回のUDFへの呼び出しを含むプランを提供します。

PRODUCTの6655行で取得するプラン

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

PRODUCTの50000行で、代わりにこのプランを取得します。

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

UDFを呼び出すためのコストは著しく過小評価されていると思います。

この場合にうまく機能する1つの回避策は、UDFに対して外部適用を使用するようにクエリを変更することです。テーブルPRODUCTの行数に関係なく、適切なプランが得られます。

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

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

この場合の最善の回避策は、おそらく必要な値を一時テーブルに入れてから、UDFに相互適用して一時テーブルを照会することです。そうすれば、UDFが必要以上に実行されないことが確実になります。

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

一時テーブルに永続化する代わりtop()に、派生テーブルで使用して、UDFが呼び出される前にSQL Serverに結合の結果を評価させることができます。SQL ServerがUDFを使用して使用できるようになる前に、クエリのその部分の行をカウントする必要があるように、最上位で本当に高い数値を使用するだけです。

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

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

すべての操作は主キーを使用して行われるため、これが原因である理由と、これを簡単に解決できないより複雑なクエリで発生した場合の修正方法を理解したいと思います。

私は本当にそれに答えることはできませんが、とにかく私が知っていることを共有すべきだと思いました。PRODUCTテーブルのスキャンが考慮される理由がまったくわかりません。それが最善の場合であり、オプティマイザーが私が知らないUDFをどのように扱うかに関するものがあるかもしれません。

余分な観察結果の1つは、新しい基数推定器を使用して、クエリがSQL Server 2014で適切な計画を取得することです。これは、UDFの各呼び出しの推定行数が、SQL Server 2012以前のように1ではなく100であるためです。ただし、計画のスキャンバージョンとシークバージョンの間で同じコストベースの決定を行います。PRODUCTの500行(私の場合は497)未満の行では、SQL Server 2014でもスキャンバージョンのプランを取得できます。


2
どういうわけか、SQLビットでアダムMachanicのセッションを思い出させる:sqlbits.com/Sessions/Event14/...
ジェームズ・Z
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.