実行プランを読む - 基本編 (その 3)

 

神谷 雅紀
Escalation Engineer

 

ビューカウントが少なかったので途中終了した「実行プランを読む」ですが、一部の方々からもうしばらく続けて欲しいという要望がありましたので、再開することにしました。

実行プランを見て、そのクエリがどのように実行されているのかを確認する際に、Stop and Go オペレータとともに注意する必要のある動きが Rebind と Rewind です。実行プランを読む - 基本編 (その 1), 実行プランを読む - 基本編 (その 2) に加えて、今回の内容を知っていれば、実行プランを見ることでクエリがどのように動くのかを理解することができるようになると思います。

 

Rebind と Rewind

Rewind とは、データセット内の現在位置を最初の行に戻す動作です。その名の通り、巻き戻し (rewind) です。これに対して Rebind は、ある条件でオペレータが実行された後、再度別の新しい条件で実行されるとともに、行位置を新しい条件によるデータセットの最初の行に移動する動作です。オペレータに新しいパラメータを結び付け直し (rebind)、再実行します。Rebind と Rewind は、どちらも Nested Loops の inner input (実行プラン表示の下側の入力) のみで行われる動きです。

 

Rebind/Rewind 回数の確認方法

SET SHOWPLAN_XML や SET STATISTICS PROFILE XML などによる XML 形式実行プランには、予想 Rebind/Rewind 回数が EstimateRebinds/EstimateRewinds として含まれています。SET SHOWPLAN_ALL や SET STATISTICS PROFILE などの regacy プランには含まれません。実際の実行回数についても、SET STATISTICS PROFILE XML には、ActualRebinds/ ActualRewinds として含まれますが、regacy プランには含まれません。

尚、SET STATISTICS PROFILE XML であっても、すべてのオペレータについて ActualRebinds/ActualRewinds が出力されるとは限りません。ActualRebinds/ActualRewinds が出力されるのは、Rebind と Rewind の動作が明確に異なるいくつかのオペレータに限定されています。例えば、Table Scan は、Rebind のために新しい条件が指定されることはないため、ActualRebinds/ActualRewinds は出力されません。一方、Row Count Spool は、Rebind によりスプールするデータの条件が変わればデータの再ロードが行われ、単に現在位置が最初の行に戻るだけの Rewind とは動作が異なるため、ActualRebinds/ActualRewinds は出力されます。ActualRebinds/ActualRewinds の出力は、具体的には、Index Spool (非クラスタ化のみ), Remote Query, Row Count Spool, Sort, Table Spool, テーブル値関数、これらに加えて、分散パーティションビューで使用される Startup 属性を持つ Filter と Assert に限定されています。これら以外のオペレータについては、Executes の値で判断します。

 

EstimateRewind の例 : Table Scan

create table t1 (c1 int not null, c2 int) create table t2 (c1 int not null, c2 int) go insert into t1 values (1,1) insert into t1 values (2,2) insert into t1 values (3,3) insert into t2 values (1,1) insert into t2 values (2,2) insert into t2 values (3,3) go set statistics xml on set statistics profile on go select t1.c2, t2.c2 from t1 inner loop join t2 on t1.c1=t2.c1 go

インデックスのないテーブルなので、テーブルに対する検索はすべて Table Scan (全検索) です。この実行プランのポイントは、Nested Loops に WHERE 句があることです。下位オペレータから返される行はフィルタされていないため、Nested Loops が行の一致不一致を判断する必要があります。

Rows Executes Stmt Text
3 1   |--Nested Loops(Inner Join, WHERE:([testdb1].[dbo].[t2].[c1]=[testdb1].[dbo].[t1].[c1]))
3 1        |--Table Scan(OBJECT:([testdb1].[dbo].[t1]))
9 3        |--Table Scan(OBJECT:([testdb1].[dbo].[t2]))

XML プランでは、t2 に対する Table Scan は以下のような出力になります。Table Scan にパラメータは渡されないため、Rewind のみが行われます。

<RelOp NodeId="2" PhysicalOp="Table Scan" LogicalOp="Table Scan" EstimateRows="3"  …  TableCardinality="3" Parallel="0" EstimateRebinds="0" EstimateRewinds="2"> <OutputList>   <ColumnReference Database="[testdb1]" Schema="[dbo]" Table="[t2]" Column="c1" />   <ColumnReference Database="[testdb1]" Schema="[dbo]" Table="[t2]" Column="c2" /> </OutputList> <RunTimeInformation>   <RunTimeCountersPerThread Thread="0" ActualRows="9" ActualEndOfScans="3" ActualExecutions="3" /> </RunTimeInformation> …

クエリの実行は、次のとおりです。t1 から 1 行取り出し、その行との比較のために t2 から 1 行読み取る。一致すればその行を結果行として出力、一致しなければ t2 から読み取った行は破棄。これを t2 の最後の行まで行う。t2 のすべての行を読み取ったら、t1 から次の行を読み取る。t2 を巻き戻して (rewind) 最初の行に位置付け、1 行読み取る。これを繰り返し、t1 のすべての行を処理したらクエリ終了。
このような動作になるため、t2 に対する Table Scan は、t1 の行数と同じ回数実行されることになります。 言い換えれば、t1 の Table Scan の Rows と t2 の Table Scan の Executes は等しくなります。t2 の Table Scan の Executes は Rewind と同一です。

 

EstimateRebind の例 : Clustered Index Seek

先ほどのテーブル t2 にクラスタ化インデックスを作成し、再度同じクエリを実行します。

alter table t2 add constraint PK_t1 primary key (c1) go

実行プランは次のようになります。この実行プランのポイントは、Nested Loops に OUTER REFERENCE があり、Clustered Index Seek 内で outer input である t1 の列値が参照されているということです。また、Inner (Clustered Index Seek) からは、outer (Table Scan) からの行に一致した行しか返されないため、Nested Loops 自体は、比較を行う必要がありません。

Rows Executes Stmt Text
3 1   |--Nested Loops(Inner Join, OUTER REFERENCES:([testdb1].[dbo].[t1].[c1]))
3 1        |--Table Scan(OBJECT:([testdb1].[dbo].[t1]))
9 3        |--Clustered Index Seek(OBJECT:([testdb1].[dbo].[t2].[PK_t1]), SEEK:([testdb1].[dbo].[t2].[c1]=[testdb1].[dbo].[t1].[c1]) ORDERED FORWARD)

XML プランでは、t2 に対する Clustered Index Seek は以下のようになります。t1 から取得した結合条件となる列の値を t2 の Clustered Index Seek に渡すため、t1 から行を取るたびに、Clustered Index Seek の検索条件は変わるため、Rebind のみが行われます。

<RelOp NodeId="2" PhysicalOp="Clustered Index Seek" LogicalOp="Clustered Index Seek" EstimateRows="1" … TableCardinality="3" Parallel="0" EstimateRebinds="2" EstimateRewinds="0">   <OutputList>     <ColumnReference Database="[testdb1]" Schema="[dbo]" Table="[t2]" Column="c2" />   </OutputList>   <RunTimeInformation>     <RunTimeCountersPerThread Thread="0" ActualRows="3" ActualEndOfScans="0" ActualExecutions="3" />   </RunTimeInformation>

クエリの実行は、次のように行われます。t1 から最初の行を読み取る。t1 から読み取ったキー値を検索条件として t2 を検索。検索条件に一致した 1 行を取り出す。主キー検索であるため結果は 1 行のみ。t1 の次の行を読み取る。t1 から読み取ったキー値を検索条件として t2 を検索。これを t1 の行がなくなるまで繰り返す。
このような動作になるため、t2 に対する Clustered Index Seek は、t1 の行数と同じ回数実行されることになります。 言い換えれば、t1 の Table Scan の Rows と t2 の Clustered Index Seek の Executes は等しくなります。t2 の Clustered Index Seek の Executes は Rebind と同一です。

 

ActualRebinds/ActualRewinds の例 : Sort

上の例に続いて、クエリに order by を追加します。

select t1.c2, t2.c2 from t1 inner loop join t2 on t1.c1=t2.c1 order by t1.c2 go

実行プランは以下のようになります。

Rows Executes Stmt Text
3 1   |--Sort(ORDER BY:([testdb1].[dbo].[t1].[c2] ASC))
3 1        |--Nested Loops(Inner Join, OUTER REFERENCES:([testdb1].[dbo].[t1].[c1]))
3 1             |--Table Scan(OBJECT:([testdb1].[dbo].[t1]))
3 3             |--Clustered Index Seek(OBJECT:([testdb1].[dbo].[t2].[PK_t1]), SEEK:([testdb1].[dbo].[t2].[c1]=[testdb1].[dbo].[t1].[c1]) ORDERED FORWARD)

XML プランでは、Sort は以下のようになります。Sort では、ActualRebinds/ActualRewinds が出力されます。

<RelOp NodeId="0" PhysicalOp="Sort" LogicalOp="Sort" …   <RunTimeInformation>     <RunTimeCountersPerThread Thread="0" ActualRows="3" ActualRebinds="1" ActualRewinds="0" ActualEndOfScans="1" ActualExecutions="1" />   </RunTimeInformation> …