これは、SQLServerの行モードの並列プランの起動方法を深く掘り下げた5部構成のシリーズの第2部です。最初のパートの終わりまでに、<em>実行コンテキストゼロを作成しました。 親タスク用。このコンテキストには実行可能演算子のツリー全体が含まれていますが、クエリ処理エンジンの反復実行モデルの準備はまだできていません。
SQL Serverは、クエリスキャンと呼ばれるプロセスを通じてクエリを実行します。 。プランの初期化は、クエリプロセッサが Open
を呼び出すことによってルートから開始されます ルートノード上。 開くコード> 呼び出しは、
Open
を再帰的に呼び出すイテレータのツリーをトラバースします ツリー全体が開かれるまで、各子に適用されます。
結果行を返すプロセスも再帰的であり、クエリプロセッサが GetRow
を呼び出すことによってトリガーされます。 ルートで。各ルート呼び出しは、一度に1行を返します。クエリプロセッサは引き続きGetRow
を呼び出します 使用可能な行がなくなるまで、ルートノードで。最後の再帰的なClose
で実行がシャットダウンします 電話。この配置により、クエリプロセッサは、ルートで同じインターフェイスメソッドを呼び出すことにより、任意のプランを初期化、実行、および閉じることができます。
実行可能演算子のツリーを行ごとの処理に適したツリーに変換するために、SQLServerはクエリスキャンを追加します。 各オペレーターへのラッパー。 クエリスキャン オブジェクトはOpen
を提供します 、 GetRow
、および Close
反復実行に必要なメソッド。
クエリスキャンオブジェクトは、状態情報も維持し、実行中に必要な他のオペレーター固有のメソッドを公開します。たとえば、スタートアップフィルター演算子のクエリスキャンオブジェクト( CQScanStartupFilterNew
)次のメソッドを公開します:
開く
-
GetRow
閉じる
-
PrepRecompute
-
GetScrollLock
-
SetMarker
-
GotoMarker
-
GotoLocation
-
ReverseDirection
休止中
このイテレータの追加のメソッドは、主にカーソルプランで使用されます。
ラッピングプロセスは、クエリスキャンの初期化と呼ばれます。 。これは、クエリプロセッサから CQueryScan ::InitQScanRoot
への呼び出しによって実行されます。 。親タスクは、計画全体に対してこのプロセスを実行します (実行コンテキストゼロに含まれます)。翻訳プロセス自体は本質的に再帰的であり、ルートから始まり、ツリーを下っていきます。
このプロセス中、各オペレーターは独自のデータを初期化し、ランタイムリソースを作成する責任があります。 が必要だ。これには、クエリプロセッサの外部に追加のオブジェクトを作成することが含まれる場合があります。たとえば、永続ストレージからデータをフェッチするためにストレージエンジンと通信するために必要な構造です。
ノード番号が追加された実行プランのリマインダー(クリックして拡大):
ルートのオペレーター 実行可能プランツリーの(ノード0)はシーケンスプロジェクト 。 CXteSeqProject
という名前のクラスで表されます 。いつものように、ここから再帰的な変換が始まります。
前述のように、 CXteSeqProject
オブジェクトは、反復的なクエリスキャンに参加するように装備されていません プロセス—必要な Open
がありません 、 GetRow
、および Close
メソッド。クエリプロセッサには、そのインターフェイスを提供するために実行可能演算子のラッパーが必要です。
そのクエリスキャンラッパーを取得するために、親タスクは CXteSeqProject ::QScanGet
を呼び出します。 タイプCQScanSeqProjectNew
のオブジェクトを返す 。 リンクされたマップ 以前に作成された演算子の数は、新しいクエリスキャンオブジェクトを参照するように更新され、そのイテレータメソッドはプランのルートに接続されます。
シーケンスプロジェクトの子はセグメントです 演算子(ノード1)。 CXteSegment ::QScanGet
を呼び出す タイプCQScanSegmentNew
のクエリスキャンラッパーオブジェクトを返します 。リンクされたマップが再度更新され、イテレータ関数ポインタが親シーケンスプロジェクトのクエリスキャンに接続されます。
次のオペレーターは、ストリームの収集交換です。 (ノード2)。 CXteExchange ::QScanGet
を呼び出す CQScanExchangeNew
を返します あなたが今までに期待しているかもしれないように。
これは、重要な追加の初期化を実行する必要があるツリーの最初の演算子です。 消費者側を作成します CXTransport ::CreateConsumerPart
を介した交換の 。これにより、ポート( CXPort
)が作成されます )—同期とデータ交換に使用される共有メモリ内のデータ構造—およびパイプ( CXPipe
)パケット転送用。 プロデューサーに注意してください 交換の側は作成されていません 現時点では。交換は半分しかありません!
次に、クエリプロセッサスキャンを設定するプロセスは、マージ結合に進みます。 (ノード3)。 QScanGet
をいつも繰り返すとは限りません およびCQScan*
この時点から呼び出しますが、確立されたパターンに従います。
マージ結合には2つの子があります。クエリスキャンのセットアップは、外側(上部)の入力(ストリームアグリゲート)で以前と同じように続行されます (ノード4)、次に再パーティションストリーム交換 (ノード5)。再パーティションストリームは、交換のコンシューマ側のみを作成しますが、今回はDOPが2であるため、2つのパイプが作成されます。このタイプの交換のコンシューマー側には、親オペレーターへのDOP接続があります(スレッドごとに1つ)。
次に、別のストリームアグリゲートがあります (ノード6)と並べ替え (ノード7)。ソートには、実行プランに表示されない子があります— tempdbへのスピルを実装するために使用されるストレージエンジンの行セット 。予想されるCQScanSortNew
したがって、子 CQScanRowsetNew
が付随します 内部ツリーで。 showplanの出力には表示されません。
I/Oプロファイリングと遅延操作
並べ替え 演算子は、これまでに初期化した最初の演算子でもあり、 I / Oの原因となる可能性があります 。実行がI/Oプロファイリングデータを要求したと仮定すると(たとえば、「実際の」計画を要求することによって)、ソートはこのランタイムプロファイリングデータを記録するオブジェクトを作成します CProfileInfo ::AllocProfileIO
経由 。
次の演算子は計算スカラーです (ノード8)、プロジェクトと呼ばれます 初めの。 CXteProject ::QScanGet
へのクエリスキャンセットアップ呼び出し しません この計算スカラーによって実行される計算は延期されるため、クエリスキャンオブジェクトを返します 結果を必要とする最初の親演算子に。この計画では、その演算子がソートです。ソートは計算スカラーに割り当てられたすべての作業を実行するため、ノード8のプロジェクトはクエリスキャンツリーの一部を形成しません。計算スカラーは実際には実行時に実行されません。遅延計算スカラーの詳細については、「計算スカラー、式、および実行プランのパフォーマンス」を参照してください。
計画のこのブランチの計算スカラーの後の最後の演算子は、インデックスシークです ( CXteRange
)ノード9で。これにより、期待されるクエリスキャン演算子( CQScanRangeNew
)が生成されます。 )が、ストレージエンジンに接続し、インデックスの並列スキャンを容易にするために、初期化の複雑なシーケンスも必要です。
ハイライトをカバーし、インデックスシークを初期化します:
- プロファイリングオブジェクトを作成します I / O用(
CProfileInfo ::AllocProfileIO
。 - 並列行セットを作成します クエリスキャン(
CQScanRowsetNew ::ParallelGetRowset
。 - 同期を設定します 実行時の並列範囲スキャンを調整するオブジェクト(
CQScanRangeNew ::GetSyncInfo
。 - ストレージエンジンのテーブルカーソルを作成します 読み取り専用のトランザクション記述子 。
- 読み取り用に親行セットを開きます(HoBtにアクセスし、必要なラッチを取得します)。
- ロックタイムアウトを設定します。
- プリフェッチを設定します (関連するメモリバッファを含む)。
これで、プランのこのブランチのリーフレベルに到達しました(インデックスシークには子がありません)。インデックスシーク用のクエリスキャンオブジェクトを作成したら、次のステップはクエリスキャンをラップすることです。 プロファイリングクラスを使用します(実際の計画を要求したと仮定します)。これは、 sqlmin!PqsWrapQScan
の呼び出しによって行われます。 。クエリスキャンが作成された後、イテレータツリーの昇順を開始するときに、プロファイラが追加されることに注意してください。
PqsWrapQScan
親として新しいプロファイリング演算子を作成します CProfileInfo ::GetOrCreateProfileInfo
の呼び出しを介した、インデックスシークの 。 プロファイリングオペレーター ( CQScanProfileNew
)通常のクエリスキャンインターフェイスメソッドがあります。プロファイリングデータは、実際の計画に必要なデータを収集するだけでなく、DMV sys.dm_exec_query_profiles
を介して公開されます。 。
現在のセッションのこの正確な時点でDMVをクエリすると、単一のプランオペレーター(ノード9)のみが存在することが示されます(つまり、プロファイラーによってラップされる唯一のオペレーターです):
このスクリーンショットは、現時点でのDMVからの完全な結果セットを示しています(編集されていません)。
次に、 CQScanProfileNew
クエリパフォーマンスカウンターAPI( KERNEL32!QueryPerformanceCounterStub
)最初と最後のアクティブ時間を記録するためにオペレーティングシステムによって提供されます プロファイルされたオペレーターの:
最後のアクティブ時間 そのイテレータのコードが実行されるたびに、クエリパフォーマンスカウンタAPIを使用して更新されます。
次に、プロファイラは推定行数を設定します。 計画のこの時点で( CProfileInfo ::SetCardExpectedRows
)、任意の行の目標を考慮します( CXte ::CardGetRowGoal
)。これは並列プランであるため、結果をスレッド数で除算します( CXte ::FGetRowGoalDefinedForOneThread
)そして結果を実行コンテキストに保存します。
推定行数は表示されません 親タスクはこのオペレーターを実行しないため、この時点でDMVを介して。代わりに、スレッドごとの見積もりは、後で並列実行コンテキスト(まだ作成されていません)で公開されます。それでも、スレッドごとの番号は親タスクのプロファイラーに保存されます。DMVからは表示されません。
わかりやすい名前 次に、プラン演算子(「インデックスシーク」)の値が CXteRange ::GetPhysicalOp
の呼び出しを介して設定されます。 :
その前に、DMVにクエリを実行すると名前が「???」と表示されることに気付いたかもしれません。これは、わかりやすい名前が定義されていない非表示の演算子(ネストされたループのプリフェッチ、バッチソートなど)に表示される永続的な名前です。
最後に、インデックスメタデータと現在の I/O統計 ラップされたインデックスのシークは、 CQScanRowsetNew ::GetIoCounters
の呼び出しを介して追加されます :
現時点ではカウンターはゼロですが、完成したプランの実行中にインデックスシークがI/Oを実行すると更新されます。
インデックスシーク用に作成されたプロファイリング演算子を使用すると、クエリスキャン処理はツリーを上に移動して親の並べ替えに戻ります。 (ノード7)。
ソートは次の初期化タスクを実行します:
- クエリメモリマネージャを使用してメモリ使用量を登録します (
CQryMemManager ::RegisterMemUsage
) - 並べ替え入力に必要なメモリを計算します(
CQScanIndexSortNew ::CbufInputMemory
)および出力(CQScanSortNew ::CbufOutputMemory
。 - 並べ替えテーブル 関連するストレージエンジンの行セット(
sqlmin!RowsetSorted
)とともに作成されます 。 - スタンドアロンのシステムトランザクション (ユーザートランザクションによって制限されない)偽の作業テーブル(
sqlmin!CreateFakeWorkTable
)とともに、スピルディスクの並べ替え割り当て用に作成されます 。 - 式サービスが初期化されます(
sqlTsEs!CEsRuntime ::Startup
)並べ替え演算子が計算を実行するための延期 計算スカラーから。 - プリフェッチ tempdbにスピルされたすべてのソート実行 次に、(
CPrefetchMgr ::SetupPrefetch
を介して作成されます 。
最後に、ソートクエリスキャンは、インデックスシークで見たように、プロファイリングオペレーター(I / Oを含む)によってラップされます。
計算スカラー(ノード8)が欠落していることに注意してください。 DMVから。これは、その作業がソートに延期され、クエリスキャンツリーの一部ではないため、ラッピングプロファイラーオブジェクトがないためです。
ソートの親であるストリームアグリゲートに移動します クエリスキャン演算子(ノード6)は、その式とランタイムカウンター(現在のグループ行数など)を初期化します。ストリームアグリゲートはプロファイリング演算子でラップされ、初期時刻が記録されます:
親の再パーティションストリーム交換 (ノード5)はプロファイラーによってラップされます(この時点では、この交換のコンシューマー側のみが存在することに注意してください):
親のストリームアグリゲートについても同じことが行われます。 (ノード4)、これも前述のように初期化されます:
クエリスキャン処理は、親のマージ結合に戻ります (ノード3)が、まだ初期化されていません。代わりに、マージ結合の内側(下)側に移動し、それらの演算子(ノード10〜15)に対して、上(外側)ブランチで実行したのと同じ詳細なタスクを実行します。
これらの演算子が処理されると、マージ結合 クエリスキャンが作成され、初期化され、プロファイリングオブジェクトでラップされます。多くのマージ結合は作業テーブルを使用するため(現在のマージ結合は1対多ですが)、これにはI / Oカウンターが含まれます:
親の収集ストリーム交換についても同じプロセスに従います。 (ノード2)コンシューマー側のみ、セグメント (ノード1)、およびシーケンスプロジェクト (ノード0)演算子。詳細については説明しません。
クエリプロファイルDMVは、プロファイラでラップされたクエリスキャンノードのフルセットを報告するようになりました。
シーケンスプロジェクト、セグメント、およびギャザーストリームのコンシューマーには、親タスクによって実行されるため、推定行数があることに注意してください。 、追加の並列タスクではありません( CXte ::FGetRowGoalDefinedForOneThread
を参照) ついさっき)。親タスクには並列ブランチで行う作業がないため、推定行数の概念は追加のタスクに対してのみ意味があります。
上記のアクティブ時間の値は、実行を停止し、各ステップでDMVスクリーンショットを撮る必要があるため、多少歪んでいます。別の実行(デバッガーを使用して導入された人為的な遅延なし)により、次のタイミングが生成されました:
ツリーは前に説明したのと同じシーケンスで構築されますが、プロセスは非常に高速で、1マイクロ秒しかありません。 最初にラップされたオペレーターのアクティブ時間(ノード9でのインデックスシーク)と最後(ノード0でのシーケンスプロジェクト)の差。
多くの作業を行ったように聞こえるかもしれませんが、親タスクのクエリスキャンツリーのみを作成したことを思い出してください。 、および取引所には消費者側のみがあります(プロデューサーはまだありません)。並列プランにもスレッドが1つしかありません(最後のスクリーンショットを参照)。パート3では、最初の追加の並列タスクの作成について説明します。