並列性を妨げない方法でユーザー定義のスカラー関数をエミュレートします


12

クエリに特定のプランを使用するようにSQL Serverをだます方法があるかどうかを確認しようとしています。

1.環境

異なるプロセス間で共有されるデータがあるとします。したがって、多くのスペースをとるいくつかの実験結果があるとします。その後、各プロセスについて、使用する実験結果の年/月を特定します。

if object_id('dbo.SharedData') is not null
    drop table SharedData

create table dbo.SharedData (
    experiment_year int,
    experiment_month int,
    rn int,
    calculated_number int,
    primary key (experiment_year, experiment_month, rn)
)
go

これで、すべてのプロセスについて、テーブルにパラメーターが保存されました

if object_id('dbo.Params') is not null
    drop table dbo.Params

create table dbo.Params (
    session_id int,
    experiment_year int,
    experiment_month int,
    primary key (session_id)
)
go

2.テストデータ

いくつかのテストデータを追加しましょう。

insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4 
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

3.結果の取得

現在、次の方法で実験結果を取得するのは非常に簡単@experiment_year/@experiment_monthです。

create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.SharedData as d
    where
        d.experiment_year = @experiment_year and
        d.experiment_month = @experiment_month
)
go

計画は素晴らしく、並行しています:

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(2014, 4)
group by
    calculated_number

クエリ0プラン

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

4.問題

しかし、データの使用をもう少し一般的にするために、別の機能が必要です- dbo.f_GetSharedDataBySession(@session_id int)。したがって、簡単な方法は、スカラー関数を作成し、変換することです@session_id-> @experiment_year/@experiment_month

create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_year
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_month
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

これで、関数を作成できます。

create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
        dbo.fn_GetExperimentYear(@session_id),
        dbo.fn_GetExperimentMonth(@session_id)
    ) as d
)
go

クエリ1プラン

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

データアクセスを実行するスカラー関数がプラン全体をシリアルにするため、プランはもちろんパラレルではないことを除いて同じです。

そこで、スカラー関数の代わりにサブクエリを使用するなど、いくつかの異なるアプローチを試しました。

create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
       (select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
       (select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
    ) as d
)
go

クエリ2プラン

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

またはを使用して cross apply

create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.Params as p
        cross apply dbo.f_GetSharedData(
            p.experiment_year,
            p.experiment_month
        ) as d
    where
        p.session_id = @session_id
)
go

クエリ3プラン

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

しかし、このクエリを、スカラー関数を使用するクエリほど優れたものにする方法を見つけることはできません。

いくつかの考え:

  1. 基本的には、SQL Serverに特定の値を事前に計算してから定数としてさらに渡すように何らかの方法で指示できるようにすることです。
  2. 中間実体化のヒントがあれば便利です。いくつかのバリアント(topを含むマルチステートメントTVFまたはcte)をチェックしましたが、これまでのところスカラー関数を持つものほど良い計画はありません
  3. SQL Server 2017- Froid:Relational Databaseでの命令型プログラムの最適化の今後の改善について知っていますが、それが役立つかどうかはわかりません。ただし、ここで間違っていることが証明されていれば良かったでしょう。

追加情報

(テーブルから直接データを選択するのではなく)関数を使用し@session_idています。これは、通常はパラメーターとして使用されるさまざまなクエリで使用する方がはるかに簡単だからです。

実際の実行時間を比較するように頼まれました。この特定の場合

  • クエリ0は約500ミリ秒実行されます
  • クエリ1は約1500ms実行されます
  • クエリ2は約1500ms実行されます
  • クエリ3は約2000ミリ秒実行されます。

プラン#2にはシークの代わりにインデックススキャンがあり、ネストループの述語によってフィルターされます。プラン#3はそれほど悪くはありませんが、それでもプラン#0よりも多くの作業を行い、動作が遅くなります。

これdbo.Paramsはめったに変更されず、通常は1〜200行程度であり、2000行を超えることはないと想定してみましょう。現在は約10列であり、あまり頻繁に列を追加することはありません。

Paramsの行数は固定されていないため、すべて@session_idの行に行があります。そこにある列の数は固定されていませんdbo.f_GetSharedData(@experiment_year int, @experiment_month int)。これはどこからでも呼び出したくない理由の1つです。そのため、このクエリに新しい列を内部で追加できます。何らかの制限がある場合でも、これに関する意見/提案を聞いてうれしいです。


Froidを使用したクエリプランは、上記のquery2のクエリプランと似ているため、この場合に達成したい解決策は得られません。
カルティク

回答:


13

今日のSQL Serverで望んでいること、つまり単一のステートメントと並列実行を、問題のレイアウトの制限内で本当に安全に達成することはできません(私が認識しているように)。

だから私の簡単な答えはノーです。この回答の残りの部分は、ほとんどの場合、それが興味がある場合の理由の説明です。

質問で述べたように、並列プランを取得することは可能ですが、主な2つの種類があり、どちらもニーズに適していません。

  1. 相関ネストされたループが結合し、ラウンドロビンが最上位でストリームを配信します。単一の行がParams特定のsession_id値に由来することが保証されている場合、並列処理アイコンでマークされていても、内側は単一のスレッドで実行されます。これが、どうやら並列プラン3がうまく機能しない理由です。実際にはシリアルです。

  2. もう1つの方法は、ネストされたループ結合の内側の独立した並列処理です。ここで独立とは、ネストされたループ結合の外側を実行しているのと同じスレッドだけでなく、スレッドが内側で起動されることを意味します。一方の外側側の行であることが保証されている場合SQL Serverは独立内側ネストされたループの並列処理をサポートし、全く相関がないパラメータ(ジョイン計画2)。

したがって、目的の相関値を持つシリアル(1つのスレッドによる)の並列プランを選択できます。または、シークするパラメーターがないためにスキャンする必要がある内側の並列プラン。(別に:それは本当にはず使用して内側の並列処理を駆動するために許可されるように正確に一つの相関パラメータのセットを、それはおそらく正当な理由のために、実装されていません)。

自然な疑問は、なぜ相関パラメーターが必要なのかということです。SQL Serverがサブクエリなどによって提供されるスカラー値を単純に直接検索できないのはなぜですか?

SQL Serverは、定数、変数、列、式の参照などの単純なスカラー参照を使用してのみ「インデックスシーク」できます(したがって、スカラー関数の結果も修飾できます)。サブクエリ(または他の同様の構造)は、単純に複雑すぎて(そして潜在的に安全ではない)ストレージエンジン全体にプッシュできません。そのため、個別のクエリプラン演算子が必要です。これには相関が必要です。つまり、必要な並べ替えの並列性はありません。

全体として、現在のところ、変数にルックアップ値を割り当て、別のステートメントの関数パラメーターでルックアップ値を使用するような方法よりも優れたソリューションは現在ありません。

今、あなたは特定のローカルな考慮事項を持っているかもしれません。SESSION_CONTEXTつまり、年と月の現在の値をキャッシュする価値があるということです。

SELECT FGSD.calculated_number, COUNT_BIG(*)
FROM dbo.f_GetSharedData
(
    CONVERT(integer, SESSION_CONTEXT(N'experiment_year')), 
    CONVERT(integer, SESSION_CONTEXT(N'experiment_month'))
) AS FGSD
GROUP BY FGSD.calculated_number;

ただし、これは回避策のカテゴリに分類されます。

一方、集計パフォーマンスが最も重要な場合は、インライン関数を使用して、テーブルに列ストアインデックス(プライマリまたはセカンダリ)を作成することを検討できます。とにかく、列ストアストレージ、バッチモード処理、および集約プッシュダウンの利点は、行モードの並列シークよりも大きな利点があることがわかります。

ただし、特に列ストアストレージでは、スカラーT-SQL関数に注意してください。これは、別の行モードフィルターで行ごとに評価される関数になりやすいためです。SQL Serverがスカラーを評価することを選択する回数を保証することは、一般的に非常に注意が必要です。


ありがとう、ポール、すばらしい答えです!私は使用session_contextすることを考えましたが、それは私にとって少しクレイジーすぎるアイデアであると判断し、現在のアーキテクチャにどのように適合するかわかりません。ただし、サブクエリの結果を単純なスカラー参照のように扱う必要があることをオプティマイザーに知らせるために使用できるヒントが役立つ場合があります。
ロマンペカール

8

私の知る限り、T-SQLだけでは望んでいる計画の形は不可能です。クラスター化インデックススキャンに対してフィルターとして直接適用される関数からのサブクエリを含む元のプランの形状(クエリ0プラン)が必要なようです。ローカル変数を使用してスカラー関数の戻り値を保持しない場合、そのようなクエリプランを取得することはありません。代わりに、フィルタリングはネストされたループ結合として実装されます。(並列処理の観点から)ループ結合を実装できる方法は3つあります。

  1. 計画全体はシリアルです。これは受け入れられません。これは、クエリ1に対して取得する計画です。
  2. ループ結合はシリアルで実行されます。この場合、内側は並行して実行できますが、述語をそれに渡すことはできません。そのため、ほとんどの作業は並行して行われますが、テーブル全体をスキャンしているため、部分集計は以前よりもはるかに高価になります。これは、クエリ2に対して取得する計画です。
  3. ループ結合は並行して実行されます。並列ネストループ結合では、ループの内側がシリアルで実行されますが、一度に内側で最大DOPスレッドを実行できます。外側の結果セットには単一の行があるだけなので、並列プランは事実上シリアルになります。これは、クエリ3に対して取得する計画です。

これらは、私が知っている唯一の可能な計画形状です。一時テーブルを使用すれば他のいくつかを取得できますが、クエリのパフォーマンスをクエリ0の場合と同じくらい良好にしたい場合は、基本的な問題を解決できません。

スカラーUDFを使用して戻り値をローカル変数に割り当て、それらのローカル変数をクエリで使用することにより、同等のクエリパフォーマンスを実現できます。そのコードをストアドプロシージャまたはマルチステートメントUDFでラップして、保守性の問題を回避できます。例えば:

DECLARE @experiment_year int = dbo.fn_GetExperimentYear(@session_id);
DECLARE @experiment_month int = dbo.fn_GetExperimentMonth(@session_id);

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(@experiment_year, @experiment_month)
group by
    calculated_number;

スカラーUDFは、並列処理に適格にするクエリの外部に移動されました。私が得るクエリプランはあなたが望むものであるように見えます:

並列クエリプラン

この結果セットを他のクエリで使用する必要がある場合、両方のアプローチには欠点があります。ストアドプロシージャに直接参加することはできません。結果を、独自の一連の問題がある一時テーブルに保存する必要があります。MS-TVFに参加できますが、SQL Server 2016では、カーディナリティの推定の問題が発生する場合があります。SQL Server 2017は、問題を完全に解決できるMS-TVFのインターリーブ実行を提供します。

T-SQL Scalar UDFは常に並列処理を禁止しており、MicrosoftはFROIDがSQL Server 2017で利用可能になるとは述べていません。


SQL 2017のFroidについて-なぜそうなったのかわからない。- vNextであることが確認されていますbrentozar.com/archive/2018/01/...
ローマPekar

4

これは、おそらくSQLCLRを使用して実行できます。SQLCLR Scalar UDFの利点の1つは、データアクセスを行わない場合は並列処理を妨げないことです(また、「決定的」としてマークする必要がある場合もあります)。では、操作自体にデータアクセスが必要な場合に、データアクセスを必要としないものをどのように利用しますか?

さて、dbo.Paramsテーブルは次のことを期待されているからです。

  1. 通常、2000行を超えることはありません。
  2. 構造をめったに変更しない、
  3. (現在)必要なのは2 INT列のみ

3つの列をキャッシュすることが可能である- session_id, experiment_year int, experiment_month-静的コレクションにのアウトプロセス人口および取得スカラUDFので読み取られ(おそらく、例えばA辞書、)experiment_year intexperiment_month値を。「アウトプロセス」とは、完全に独立したSQLCLR Scalar UDFまたはストアドプロシージャを使用して、データアクセスとdbo.Paramsテーブルからの読み取りを行って、静的コレクションを作成できることを意味します。そのUDFまたはストアドプロシージャは、「年」および「月」の値を取得するUDFを使用する前に実行され、「年」および「月」の値を取得するUDFはDBデータアクセスを行いません。

データを読み取るUDFまたはストアドプロシージャは、コレクションに0エントリがあるかどうかを最初に確認し、ある場合は値を設定し、そうでない場合はスキップします。移入された時間を追跡し、X分(またはそのようなもの)を超えている場合は、コレクションにエントリがある場合でも消去して再移入することもできます。ただし、2つのメインUDFが値を取得するために常にデータが入力されるようにするために、ポピュレーションを頻繁に実行する必要があるため、ポピュレーションをスキップすると役立ちます。

主な懸念は、SQL Serverが何らかの理由でアプリドメインをアンロードすることを決定した場合(またはを使用してトリガーされた場合DBCC FREESYSTEMCACHE('ALL');)です。"populate" UDFまたはストアドプロシージャの実行とUDFの間にコレクションがクリアされて "year"および "month"の値を取得するリスクは避けたいものです。その場合、これらの2つのUDFの最初にチェックを入れて、コレクションが空の場合に例外をスローすることができます。これは、誤った結果を提供するよりもエラーを起こす方がよいためです。

もちろん、上記の懸念事項は、アセンブリにとしてマークを付けることを望んでいることを前提としていSAFEます。アセンブリをとしてマークできる場合、EXTERNAL_ACCESS静的コンストラクターにデータを読み取り、コレクションにデータを取り込むメソッドを実行させることができます。そのため、行を更新するために手動でそれを実行するだけでよいのですが、常にデータが取り込まれます。 (静的クラスコンストラクターは、クラスが読み込まれたときに常に実行されるため、再起動またはApp Domainがアンロードされた後にこのクラスのメソッドが実行されるたびに発生します)。これには、インプロセスContext Connectionではなく、通常の接続を使用する必要があります(静的コンストラクターでは利用できないため、が必要ですEXTERNAL_ACCESS)。

注:アセンブリをとしてマークする必要がないようにするにはUNSAFE、静的クラス変数をとしてマークする必要がありますreadonly。これは、少なくともコレクションを意味します。読み取り専用のコレクションではアイテムを追加または削除できるため、これは問題ではありません。コンストラクターまたは初期ロード以外では初期化できません。X分後にコレクションを期限切れにする目的でロードされた時間を追跡するのstatic readonly DateTimeは、コンストラクターまたは初期ロード以外ではクラス変数を変更できないため、扱いにくいです。この制限を回避するには、DateTime値である単一のアイテムを含む静的な読み取り専用コレクションを使用して、更新時に削除して再追加できるようにする必要があります。


なぜ誰かがこれを否定したのか分かりません。あまり一般的ではありませんが、現在のケースに適用できると思います。私は純粋なSQLソリューションを持っていることを好むだろうが、私は間違いなくこの詳しく見て取り、作品かどうかを確認してみましょう
ローマPekar

@RomanPekar確かではありませんが、反SQLCLRの人々がたくさんいます。そして、たぶんいくつかはアンチミーです;-)。いずれにせよ、このソリューションが機能しない理由は考えられません。私は純粋なT-SQLの好みを理解していますが、それを実現する方法がわかりません。競合する答えがなければ、他の人もそうしません。ここでは、メモリ最適化テーブルとネイティブコンパイルされたUDFの方が良いかどうかわかりません。また、念頭に置いていくつかの実装ノートを含む段落を追加しました。
ソロモンラッツキー

1
readonly staticsSQLCLR での使用が安全または賢明であると確信したことはありません。私はそれをreadonly参照型にしてシステムをだますことを確信しており、それを参照型に変更します。私に絶対的な意志を与えます。
ポールホワイト9

@PaulWhite了解しました。数年前に個人的な会話でこのことを思い出しました。staticSQL Server のアプリドメイン(およびオブジェクト)の共有性を考えると、はい、競合状態のリスクがあります。そのため、最初にOPからこのデータが最小限で安定していると判断し、このアプローチを「まれに変更する」必要があると判断し、必要に応じて更新する手段を提供しました。で、このいずれかのリスク場合のユースケース私はあまり見ません。数年前に、読み取り専用コレクションを設計どおりに更新する機能についての記事を見つけました(C#では、議論はありません:SQLCLR)。それを見つけようとします。
ソロモンラッツキー

2
必要はありませんが、SQL Serverの公式ドキュメントでは問題ないということは別として、これに満足する方法はありません。
ポールホワイト9
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.