小物SEのメモ帳

IT関係の試したことや調べたことの備忘録

【SQL Server】クラスター化インデックスと非クラスター化インデックス

SQLserverのインデックスのうちよく使われる基本的なインデックスであるクラスター化インデックス、非クラスター化インデックス、付加列について解説します。これらはSQLserverのクエリをチューニングするうえでの基礎となるので押さえておくとよいでしょう。
以下のようなテーブルを例に説明します。
f:id:utiowa:20170205231657p:plain

1.クラスターインデックス

クラスターインデックスは以下図のような構成となっています。
例)EMPLOYEE_ID = '1002'を検索した場合
f:id:utiowa:20170205231715p:plain

ポイントとしては、クラスターインデックスのリーフノードにはテーブルのデータ部分が含まれていることでしょうか。
クラスターインデックスがないテーブルの場合ヒープになりますが、ここではヒープの説明は割愛させていただきます。


2.非クラスターインデックス

クラスターインデックスは以下図のような構成となっています。
例)FIRST_NAME = 'Arizabeth' を検索した場合
f:id:utiowa:20170205232509p:plain

クラスターインデックスのリーフには、実データへの参照先として、ヒープの場合は行識別子、クラスターインデックスが存在する場合はクラスターインデックスのキー列が含まれています。
クラスターインデックスのリーフだけで参照列が不足している場合には、行識別子やクラスターインデックスのキー列を使用してデータを取得します。

3.付加列

クラスターインデックスに付加列を追加した場合は以下図のような構成となります。
例)FIRST_NAME = 'Arizabeth' を検索し、FIRST_NAME、LAST_NAME,EMAILを取得したい場合
f:id:utiowa:20170205231809p:plain

WHERE句に使用される列を付加列を追加しても効果はありませんが、取得したい列が追加である場合に有効かと思います。

尚、複合インデックスの場合は中間リーフにもその列は含むことになります。WHERE句に使っていないが、参照したい列がある場合は付加列のインデックスを使うとよいでしょう。
特にその列のデータ長が大きい場合、複合インデックスにすると中間リーフの1ページあたりのデータ量が多くなってしまうので、付加列を使用することで中間リーフの1ページあたりのデータの格納効率がよくなります。


実際にクエリを実行して確認してみます。
各種インデックスは次の通り。
f:id:utiowa:20170205231841p:plain
f:id:utiowa:20170205231845p:plain
f:id:utiowa:20170205231851p:plain
f:id:utiowa:20170205231857p:plainf:id:utiowa:20170205231901p:plain

複合インデックスに含まれない列を参照した場合は、下記のようにキー参照(Key Lookup)が発生しています。
f:id:utiowa:20170205231916p:plain

クラスターインデックス(NCIX_1)にはEMAIL列を含んでいないため、先に非クラスターインデックスを参照した後、キー参照にてクラスターインデックスを参照しています。
イメージ的には以下のような形でしょうか。
f:id:utiowa:20170205231936p:plain

実際にページの読み取り数を確認すると、論理読み取り数が4であることが確認できます。
f:id:utiowa:20170205231945p:plain

複合インデックスに含まれる列だけを参照した場合は、下記のようにキー参照(Key Lookup)が発生していないことがわかります。
f:id:utiowa:20170205231955p:plain

実際にページの読み取り数を確認すると、論理読み取り数が2であり、非クラスターインデックスのみでデータを取得できていることがわかります。
f:id:utiowa:20170205232006p:plain

ちなみに今回は、下記のとおりインデックスの奥行はどちらも2であるため、各インデックスのページ読み取りは2でしたが、奥行が深くなると読み取り数はそれに応じて増えるかと思います。
f:id:utiowa:20170205232019p:plain
f:id:utiowa:20170205232023p:plain
f:id:utiowa:20170205232033p:plain

キー参照が発生するとその分参照するページ数が増加するので、極力減らすことが基本となります。

同様に複合インデックスと付加列で参照できる場合は、下記のようにキー参照(Key Lookup)が発生していないことがわかります。
f:id:utiowa:20170205232108p:plain
f:id:utiowa:20170205232114p:plain

ちなみにインデックスの奥行を調べたい場合は以下のような手順で確認できます。
①オブジェクトエクスプローラーで調べたいインデックスを選択して、右クリックを押下し、「プロパティ(R)」を選択
f:id:utiowa:20170205232132p:plain

②左ペインの「断片化」を選択
f:id:utiowa:20170205232142p:plain

インデックスのページ数やリーフレベルの行、奥行をクエリで調べるなら下記のようなクエリで検索できます。
※他の調査用に使っていたものの使いまわしであまりイケてないですが。

SELECT 
    DB_NAME() AS [db_name]
	, SCHEMA_NAME(so.schema_id) AS [schema_name]
	, OBJECT_NAME(si.object_id) AS [Table_name]
	, si.name
	, si.type_desc
	, ips.alloc_unit_type_desc
	, ips.page_count
	, SUBSTRING(idxcolinfo.idxcolname,1,LEN(idxcolinfo.idxcolname) -1) AS [INDEX_Columns]
	, SUBSTRING(idxinccolinfo.idxinccolname,1,LEN(idxinccolinfo.idxinccolname) -1) AS [INDEX_Columns_Include]
	, dps.row_count
FROM		sys.dm_db_index_physical_stats (DB_ID(), NULL , NULL , NULL , 'DETAILED' ) AS ips,	
			sys.indexes AS si
LEFT JOIN	sys.dm_db_index_usage_stats AS ius
	ON	ius.object_id = si.object_id
	AND	ius.index_id = si.index_id
	AND	ius.database_id = DB_ID()
LEFT JOIN	sys.dm_db_partition_stats AS dps
	ON	si.object_id = dps.object_id
	AND	si.index_id = dps.index_id
LEFT JOIN	sys.objects so
	ON	si.object_id = so.object_id
LEFT JOIN	sys.dm_db_index_operational_stats(DB_ID(), NULL, NULL, NULL) AS ios
	ON	ios.object_id = si.object_id
	AND	ios.index_id = si.index_id
	AND	ios.partition_number = dps.partition_number
CROSS APPLY
	(SELECT sc.name + ','
		FROM			sys.index_columns AS sic
		INNER JOIN	sys.columns AS sc
		ON	sic.object_id = sc.object_id
		AND	sic.column_id = sc.column_id
		WHERE	
				sic.object_id = si.object_id
		AND	sic.index_id = si.index_id
		AND	sic.is_included_column = 0
	FOR XML PATH('') ) AS idxcolinfo(idxcolname)
CROSS APPLY
	(SELECT sc.name + ','
		FROM			sys.index_columns AS sic	
		INNER JOIN	sys.columns AS sc
		ON	sic.object_id = sc.object_id
		AND	sic.column_id = sc.column_id
	WHERE
				sic.object_id = si.object_id
		AND	sic.index_id = si.index_id
		AND	sic.is_included_column = 1
	FOR XML PATH('')) AS idxinccolinfo(idxinccolname)
WHERE
			(ius.database_id = DB_ID() OR ius.database_id IS NULL) 
	AND (ips.database_id = DB_ID() OR ius.database_id IS NULL)
	AND	so.schema_id <> SCHEMA_ID('sys')
	AND	ips.object_id	= si.object_id 
	AND ips.index_id	= si.index_id 
	AND ips.object_id	= so.object_id
	AND ips.object_id	= ios.object_id
	AND ips.index_id	= ios.index_id
	AND ips.partition_number = ios.partition_number
ORDER BY
	Table_name,
	name,
	page_count

出力結果は次のようになります。
f:id:utiowa:20170205232240p:plain