SE の雑記

SQL Server の情報をメインに Microsoft 製品の勉強内容を日々投稿

シンプルなクエリで見る結合演算子の操作の違い

leave a comment

クエリ チューニングの高度な概念 に記載されていますが、結合演算子の違いについて、シンプルなクエリを使って、軽くまとめてみたいと思います。

■ベースとなるテーブルと初期データ


今回使用するクエリのベースとなるテーブルについては、以下のものを使用します。

SET NOCOUNT ON
GO
DROP TABLE IF EXISTS T1
DROP TABLE IF EXISTS T2
GO
CREATE TABLE T1 (C1 int, C2 uniqueidentifier)
CREATE TABLE T2 (C1 int, C2 uniqueidentifier)
GO
SET NOCOUNT ON
GO
DECLARE @cnt int = 1
BEGIN TRAN
WHILE(@cnt <= 10000)
BEGIN
	INSERT INTO T1 VALUES(@cnt, NEWID())
	INSERT INTO T2 VALUES(@cnt, NEWID())
	SET @cnt += 1
END
COMMIT TRAN
GO

今回はシンプルなクエリで実行しますので、簡単なテーブル構造としています。
データの内容として、T1.C1 と T2.C1 は 1:1 なのですが、テーブルの定義としてはなにも設定していない状態をスタートとしています。
 
image

それでは、各結合の方式を確認してみたいと思います。
今回は明示的に結合にヒントを設定することで特定の操作を行うようにしています。
各結合のクエリを実行する前には、テーブルのインデックスの状態は未設定の状態としています。

■ループ結合


ループ結合でデータを取得するために、以下のクエリを実行します。

SET STATISTICS IO ON
GO
SELECT
	T1.C1,
	T2.C2
FROM
	T1
	LEFT LOOP JOIN T2
	ON T1.C1 = T2.C1
GO

 
この時の実行プランが以下になります。
image
結合については、Nested Loop となっていることが確認できますね。
Nested Loops プラン表示操作
このプランで確認するポイントは、以下の個所となります。
image
今回は、T1 / T2 ともに、10,000 件のデータを格納しています。
ループ結合の場合は、ベースとなるテーブルを元に、参照テーブルで合致する行があるかを各行に対して確認する必要があります。
今回は、T1 がベースとなるテーブルとなり、T2 が参照テーブルとなっていますので、T2 から、100,000,000 (T1 : 10,000 × T2 : 10,000)? 件の行を確認していることが、実行プランから確認できますね。
T2 のデータに関しては、一度ワークテーブルに格納され、そこから参照が行われていますので、STATISTICS IO の情報は以下のような形で出力されています。

Warning: The join order has been enforced because a local join hint is used.
Table 'T2'. Scan count 4, logical reads 152, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 4, logical reads 550712, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T1'. Scan count 5, logical reads 38, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 
ループ結合については、ベーステーブルの各行に対して、参照テーブルから合致する行があるかを走査する必要があるため、データ件数によって、どの程度の情報が流れているのかを実行プランから把握することは重要となります。
今回のケースであれば、インデックスを設定し、行の特定がシンプルになることで T2 から確認しなくてはいけない走査数については減らすことができます。

CREATE NONCLUSTERED INDEX NCIX_T2_C1 ON T2 (C1) INCLUDE(C2)
GO

 
image
「Table Spool」の操作がなくなっていますので、ワークテーブルの作成がなくなっていることも確認できますね。

Warning: The join order has been enforced because a local join hint is used.
Table 'T2'. Scan count 10000, logical reads 21352, physical reads 0, read-ahead reads 5, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T1'. Scan count 1, logical reads 38, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

■マージ結合


マージ結合でデータを取得するために、以下のクエリを実行します。
# 自分の環境では、後述の制約の設定後に並列クエリではなくなったため、設定前後でプランの条件をそろえるために MAXDOP 1 を設定しています。

SET STATISTICS IO ON
GO
SELECT
	T1.C1,
	T2.C2
FROM
	T1
	LEFT MERGE JOIN T2
	ON T1.C1 = T2.C1
OPTION (MAXDOP 1)
GO

 
この時の実行プランが以下になります。
image
マージ結合が使われていることが確認できますね。
Merge Join プラン表示操作
マージ結合は「ソートマージ結合」とも呼ばれ、両テーブルが結合列を基準にソートされている状態で、比較を行っています。
そのため、現状はインデックスを設定していませんので両テーブルでソートが発生していることが確認できますね。
 
それでは T1 / T2 にインデックスを設定してみます。

CREATE NONCLUSTERED INDEX NCIX_T1_C1 ON T1 (C1)
GO
CREATE NONCLUSTERED INDEX NCIX_T2_C1 ON T2 (C1) INCLUDE(C2)
GO

インデックスを設定したことで、ソートがなくなり、シンプルな実行プランになりましたね。
image
マージ結合については、確認のポイントはもう一つあります。
それが、「Merge ManyToMany」(多対多) の操作となります。
これについては、以下の記載がわかりやすいかと。
マージ結合について

Merge Join 操作は、標準の操作または多対多操作のいずれかになります。多対多のマージ結合では、行を格納するために一時テーブルが使用されます。各入力に重複する値がある場合、一方の入力の各重複値が処理されるとき、他方の入力は重複値の先頭に戻る必要があります

Merge Join プラン表示操作

Merge Join 操作の Argument 列には、この操作によって一対多の結合が実行されている場合は MERGE:() 述語が、多対多の結合が実行されている場合は MANY-TO-MANY MERGE:() 述語が設定されます。また、Argument 列には、操作の実行に使用される列のコンマ区切りリストも設定されます。Merge Join 操作には、それぞれの列を基準に並べ替えられた 2 つの入力が必要です。この並べ替えを行うときは、クエリ プランに明示的な並べ替え操作を挿入することが可能です。明示的な並べ替えが必要でない場合、Merge Join 操作は特に効果的です。たとえば、データベースに適切な B ツリー インデックスがある場合、またはマージ結合とロールアップを含むグループ化など、複数の操作で並べ替え順序を利用できる場合などです。

今回、格納されているデータとしては、1 対 1 となっているのですが、現状のマージ結合は多対多の操作が行われています。
image
多対多の操作を行うための一時テーブルの作成については STATISTICS IO の情報から確認することができます。
# パフォーマンスモニターの「SQLServer:Access Methods\Worktables Created/sec」で確認することもできます。

Warning: The join order has been enforced because a local join hint is used.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T2'. Scan count 1, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T1'. Scan count 1, logical reads 25, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 
データとして 1 対 1 となるように格納されていても、SQL Server はそのようなデータとなっているという判断をすることができません。
それでは、以下のクエリを実行して制約を付けてみたいと思います。

ALTER TABLE T1 ADD CONSTRAINT AK_T1_C1 UNIQUE (C1)
GO

これで、結合列に使用している T1 の C1 はユニークになっているということが SQL Server が判断できるようになりました。
設定後の実行プランがこちらです。
image
パッと見は変わっていないように見えますが、「Merge Join」のコストが変わっている (98% → 39%) ことが確認できますね。
一意制約を設定したことで、「多対多」が「False」になりました。
image
これにより、STATISTICS IO の結果から Worktable の情報がなくなっていることが確認できますね。

Warning: The join order has been enforced because a local join hint is used.
Table 'T2'. Scan count 1, logical reads 45, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T1'. Scan count 1, logical reads 25, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Worktable については tempdb の負荷となるケースがありますので、多対多の状態がどのようになっているかを意識することも、マージ結合を使用する場合のポイントとなります。

■ハッシュ結合


ハッシュ結合でデータを取得するために、以下のクエリを実行します。

SET STATISTICS IO ON
GO
SELECT
	T1.C1,
	T2.C2
FROM
	T1
	LEFT HASH JOIN T2
	ON T1.C1 = T2.C1
GO

 
この時の実行プランが以下になります。
image
ハッシュ結合が行われていることが確認できますね。
Hash Match プラン表示操作
ハッシュについては、ハッシュテーブルを作成するための Worktable や、Workfile の作成が行われていることが、STATISTICS IO から確認できます。

Warning: The join order has been enforced because a local join hint is used.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T2'. Scan count 1, logical reads 38, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'T1'. Scan count 1, logical reads 38, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

こちらもインメモリで実行されない場合は、tempdb の負荷が発生しますので、警告が発生していないかは意識する必要があります。

Share

Written by Masayuki.Ozawa

1月 7th, 2017 at 10:45 am

Posted in SQL Server

Tagged with

Leave a Reply