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プラン
しかし、このクエリをスカラー関数を使用するクエリと同じように書く方法が見つかりません。
いくつかの考え:
(テーブルから直接データを選択するのではなく)関数を使用しています。通常、パラメーターとして_@session_id
_を使用する多くの異なるクエリで使用する方がはるかに簡単だからです。
実際の実行時間を比較するように求められました。この特定のケースでは
プラン#2にはシークの代わりにインデックススキャンがあり、ネストされたループの述語によってフィルター処理されます。プラン#3はそれほど悪くはありませんが、それでもより多くの作業を行い、そのプラン#0の動作は遅くなります。
_dbo.Params
_はめったに変更されず、通常は行が1から200程度、それ以下であると想定します。たとえば、2000が予想されるとしましょう。現在は約10列ですが、あまり頻繁に列を追加することはありません。
Paramsの行数は固定されていないため、すべての_@session_id
_に対して行が存在します。固定されていない列の数は、どこからでもdbo.f_GetSharedData(@experiment_year int, @experiment_month int)
を呼び出したくない理由の1つなので、このクエリに内部的に新しい列を追加できます。いくつかの制限がある場合でも、私はこれについての意見/提案を聞いていただければ幸いです。
(私がそれらを認識しているように)質問で提示された制限の範囲内で、今日のSQL Serverで、つまり単一のステートメントで、並列実行を使用して、希望どおりの結果を本当に安全に達成することはできません。
だから私の簡単な答えはnoです。この回答の残りの部分は、それが興味がある場合に備えて、主にその理由の説明です。
質問にあるように、並列プランを取得することは可能ですが、2つの主要な種類があり、どちらもニーズに適していません。
相関のあるネストされたループが結合し、ラウンドロビンがトップレベルのストリームを配布します。特定のsession_id
値に対して単一の行がParams
から取得されることが保証されている場合、並列処理アイコンでマークされていても、内部は単一のスレッドで実行されます。これが、見かけ上並列の計画も機能しない理由です。それは実際にはシリアルです。
もう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がスカラーを評価することを選択する回数を保証することは、一般的に非常にトリッキーです。
私が知る限り、あなたが望む計画の形はT-SQLだけでは不可能です。クラスター化インデックススキャンに対してフィルターとして直接適用される関数からのサブクエリを含む元のプラン形状(クエリ0プラン)が必要なようです。ローカル変数を使用してスカラー関数の戻り値を保持しない場合、このようなクエリプランは得られません。代わりに、フィルタリングはネストされたループ結合として実装されます。ループ結合を実装するには、(並列処理の観点から)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スカラーUDFは常に並列処理を禁止しており、MicrosoftはFROIDがSQL Server 2017で利用可能になるとは言っていません。
これはほとんどの場合、SQLCLRを使用して行うことができます。 SQLCLRスカラーUDFの利点の1つは、並列処理を妨げないことですifそれらはnotデータアクセスを実行します(「決定的」としてマークする必要がある場合もあります)。では、操作自体がデータアクセスを必要とするときに、データアクセスを必要としないものをどのように利用しますか?
まあ、dbo.Params
テーブルが期待されているので:
INT
列のみが必要です3つの列(session_id, experiment_year int, experiment_month
)を、アウトプロセスで入力され、experiment_year int
とexperiment_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
値である単一のアイテムを含む静的な読み取り専用のコレクションを使用して、更新時に削除および再追加できるようにする必要があります。