web-dev-qa-db-ja.com

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

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プラン

enter image description here

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プラン

enter image description here

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

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

_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プラン

enter image description here

または _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プラン

enter image description here

しかし、このクエリをスカラー関数を使用するクエリと同じように書く方法が見つかりません。

いくつかの考え:

  1. 基本的には、SQL Serverに特定の値を事前に計算してから定数としてさらに渡すように指示できるようにすることです。
  2. 中間マテリアライズ ヒントがあった場合に役立つでしょう。私はいくつかのバリアント(マルチステートメントTVFまたはトップのcte)をチェックしましたが、これまでのところ、スカラー関数を使用するプランほど優れたプランはありません。
  3. SQL Server 2017の今後の改善について知っています- Froid:リレーショナルデータベースでの命令型プログラムの最適化 。しかし、それが役立つかどうかはわかりません。しかし、ここで間違っていることが証明されたのは素晴らしいことです。

追加情報

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

実際の実行時間を比較するように求められました。この特定のケースでは

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

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

_dbo.Params_はめったに変更されず、通常は行が1から200程度、それ以下であると想定します。たとえば、2000が予想されるとしましょう。現在は約10列ですが、あまり頻繁に列を追加することはありません。

Paramsの行数は固定されていないため、すべての_@session_id_に対して行が存在します。固定されていない列の数は、どこからでもdbo.f_GetSharedData(@experiment_year int, @experiment_month int)を呼び出したくない理由の1つなので、このクエリに内部的に新しい列を追加できます。いくつかの制限がある場合でも、私はこれについての意見/提案を聞いていただければ幸いです。

12
Roman Pekar

(私がそれらを認識しているように)質問で提示された制限の範囲内で、今日のSQL Serverで、つまり単一のステートメントで、並列実行を使用して、希望どおりの結果を本当に安全に達成することはできません。

だから私の簡単な答えはnoです。この回答の残りの部分は、それが興味がある場合に備えて、主にその理由の説明です。

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

  1. 相関のあるネストされたループが結合し、ラウンドロビンがトップレベルのストリームを配布します。特定のsession_id値に対して単一の行がParamsから取得されることが保証されている場合、並列処理アイコンでマークされていても、内部は単一のスレッドで実行されます。これが、見かけ上並列の計画も機能しない理由です。それは実際にはシリアルです。

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

したがって、必要な相関値を持つ(1つのスレッドにより)シリアルである並列プランを選択できます。または、シークするパラメータがないためにスキャンする必要がある内側の並列プラン。 (余談ですが、実際にはoughtexactly one相関パラメータのセットを使用して内側の並列処理を実行することを許可されていますが、おそらく正当な理由で)実装されていません。

その場合、自然な問題は次のとおりです。なぜ相関パラメータが必要なのでしょうか。 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がスカラーを評価することを選択する回数を保証することは、一般的に非常にトリッキーです。

13
Paul White 9

私が知る限り、あなたが望む計画の形は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が、並列処理の対象となるクエリの外部に移動されました。私が取得するクエリプランは、必要なもののようです。

parallel query plan

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

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

8
Joe Obbish

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

まあ、dbo.Paramsテーブルが期待されているので:

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

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

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

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

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

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

4
Solomon Rutzky