インデックスと実行計画を理解する@SQLServer

#免責事項
この記事は個人メモとして書き留めておいたものを、分かりやすく纏めてみたものです。記事内容に間違い・補足事項等の指摘があれば適宜修正したいと思いますが、記事の内容を100%保証するものではありません。予めご了承ください。

#はじめに
（私だけかもしれませんが）インデックスがきちんと利用されているかを、実行計画で確認したりせずに、永らくSQLを利用してきました。『（最近の）オプティマイザは優秀なので、ある程度は自動的に的確な形でSQL実行計画を立ててくれる』という記事を鵜呑みにしてきたからです。それはきっとある程度正しく、またよく使うクエリはDB内でのキャッシュ機構が働くため、中小規模のシステムであれば、問題が表面化することも少ないと思います。
しかし、何事でもそうですが、特にIT系の仕事に於いては、きちんと『基本』を抑えておくことは極めて重要です。
この記事は、SQLServerに作成したテストデータの実行計画（SQL実行プラン）を題材として、インデックスと実行計画についての基礎的な理解を深める一助となることを目的としています。


#『実行計画の見かた』について
ここでは説明を省かせていただきます。
https://use-the-index-luke.com/ja/sql/explain-plan
などを参考にしてください。
本記事の閲覧にあたっては、基本的に『Seek』は索引機能が利用されていて、『Scan』は"全件捜査"しながら条件を満たすかチェックされるという点だけ、抑えてあれば十分です。
一般には『Seek』になっていれば索引が使われ、効率的な処理が行われています。
ただ、後述しますが、データの実情によっては、必ずしも『Seek』が使われていれば高速だとは限りません。必要に応じてWITH句で利用するインデックスを指定できるのは、DBのオプティマイザ（クエリの最適化を行うエンジン）が必ずしも常に最適な判断をしているわけではないことの裏返しでもあります。そして、優秀だが万能ではないオプティマイザを補完できるのは、人間の頭脳以外にありません。我々（私だけ？）もインデックスと実行計画について理解を深めなければならない理由が、そこにあります。


以下、本題となりますので、である調に変更します。

#テストデータについて
テスト用に用意したseiseki（成績）テーブルの構成は以下のとおり。
![002.png](https://qiita-image-store.s3.amazonaws.com/0/234814/b513d008-06db-6ddf-9297-86f5150c2d4e.png)

データは以下のように用意した。
生徒と国語・算数・理科・社会のテスト点数を格納しているが、特定の点数の生徒数が偏らないよう、現実味のない点数となっていることについてはご理解頂きたい。
![003.png](https://qiita-image-store.s3.amazonaws.com/0/234814/305ccf93-a293-93e0-0c5d-bb88a68389c2.png)

件数は100万件。
usernameは「山田太郎+idの下4桁(ただし先頭0は省く)」をセット。(同一氏名が100人いる。)
classはidの下3桁で文字列型。0～999のクラスに各1000人の生徒が所属している。


インデックスは以下のとおり作成した。
氏名。
![004.png](https://qiita-image-store.s3.amazonaws.com/0/234814/37a8bf8b-8b26-8f71-fcb2-a84b1e12c123.png)
クラス。
![005.png](https://qiita-image-store.s3.amazonaws.com/0/234814/b510d5f9-923d-7bd7-b7ed-81781309b718.png)
国語、算数、理科の点数。（敢えて社会は除く。）
![006.png](https://qiita-image-store.s3.amazonaws.com/0/234814/820ce001-2e45-384c-e00a-26acecc7eb4f.png)

ディスク使用量。
実データ83MBに対して、インデックスが64MB。
![007.png](https://qiita-image-store.s3.amazonaws.com/0/234814/883d7e17-07b3-964d-3b98-22c0dd85a1a6.png)



#検証①　数値型の変換ルールとインデックス

- SQLServer（に限らない話だが）は、文字型と数値型の比較では、数値型にして比較しようとする
- そのためインデックスが貼ってあるカラムが、文字型の場合、検索条件として数値を指定するとインデックスが使用されない
- <font color="Red">where句の検索条件は、数値を文字型として指定しても問題ないが、一方で、数値型では問題が起きることがある</font>

***
###検証内容

文字型のclass列に対して、SQLで検索条件を数値型にすると、低速なIndex Scanになる。
※列classの実際の値を、逐次、数値に変換して比較となるため、インデックスは機能しない。

![001.png](https://qiita-image-store.s3.amazonaws.com/0/234814/4a457474-16b1-cd9e-0454-610fe07ff593.png)

SQLで検索条件を、文字列型にすると、高速なIndex Seekになる。

![102.png](https://qiita-image-store.s3.amazonaws.com/0/234814/f2c7f097-a5aa-c42e-ee30-708088d1d5a3.png)

逆の事例を挙げる。
国語と算数の点数(数値型)にはインデックスを貼っている。
数値型の検索条件を指定すると、当然機能する。

![103.png](https://qiita-image-store.s3.amazonaws.com/0/234814/866e8adb-dd6b-e7ec-b59c-a9b4595a4cc8.png)

<font color="#63C46B">※なお、ある程度、各テスト教科の点数は分散されるようにしていて、国語が77点の生徒はおよそ1万人前後、さらに且つ算数が77点の生徒はおよそ100人前後となるようにしている</font>

一方で、文字列型を指定した場合も、数値型で比較しようとするため、文字列77は、数値77に変換された後で動作し、高速なIndex Seekで動作する。

![104.png](https://qiita-image-store.s3.amazonaws.com/0/234814/761346bd-7605-26e0-0115-2f0c8132e847.png)

左側を文字列にしたり、検索条件の順番を入れ替えてもIndex Seekとなり、正しく処理される。

![105.png](https://qiita-image-store.s3.amazonaws.com/0/234814/793f2fa7-63ca-b0b4-bd4e-afa7879f240e.png)

#検証②　like句とインデックス	
- like句では、前方一致でインデックスが効果的と判断された場合に機能する。
- 後方一致（および部分一致）ではインデックスは機能しない
- データの実情によっては、前方一致でのインデックス活用は逆効果となる場合もあり、インデックスを利用しないようWITH句にてヒントを明示したほうがいい場合もある

***
###検証内容	
次にlike文による検証を示す。

like文により前方一致させた場合、下記の結果件数は、555、および5550～5559の1100件となる。
![301.png](https://qiita-image-store.s3.amazonaws.com/0/234814/2e84b143-31f8-1a3d-a849-7405b60d713f.png)

この場合、オプティマイザは効率がいいと判断して、Index Seekが利用される。
![302.png](https://qiita-image-store.s3.amazonaws.com/0/234814/19b71716-3937-7981-f06e-54b827243ea1.png)

なお、この場合、シーク述語としては、文字ソート順による不等号での条件指定に変換される。
![303.png](https://qiita-image-store.s3.amazonaws.com/0/234814/1bdb1160-d118-f0b2-b14b-5ccb3ef86fec.png)

中間にワイルドカードがある場合でも利用される。
![304.png](https://qiita-image-store.s3.amazonaws.com/0/234814/4bd9f99d-9abc-0831-b615-4d027cc212af.png)

この場合の、シーク述語は、「山田太郎5～山田太郎6の範囲指定」となっている。※ソート順で、6は5の次の文字のため。
![305.png](https://qiita-image-store.s3.amazonaws.com/0/234814/d4e11d3c-a140-5a9f-bbfd-9086fab0c23b.png)

当然ながら、後方一致にした場合は、Index Scanとなり、Indexの全件スキャンとなるため、索引としての機能は機能しない。
![306.png](https://qiita-image-store.s3.amazonaws.com/0/234814/f2664f98-ab07-7103-7060-dcbfb6746acf.png)

ここで、実行プランにて、Index Seekであれば、高速であるとも限らないことに注意されたい。
下記のシーク述語は、インデックスが全て「山田太郎」で始まるため、事実上、IX_seiseki_1の全件スキャンとなる。
![307.png](https://qiita-image-store.s3.amazonaws.com/0/234814/7534371a-fd14-2b18-f30d-57c6176e9347.png)


この場合、直接テーブルスキャンを行ったほうが速い可能性もある。
インデックスとして、主キーのPK_seisekiを明示的に指定した場合と比較してみる。
下図で、理論上のコストでは、32:68のコスト比となっていて、PK_seisekiを使用したほうが遅いということになっている。
WITH句なしでは、自動判定で、IX_seiseki_1が効率がいいとして使用されているので、当然なのだが、
あくまでオプティマイザが理論的(または統計的)に判定した予測であって、現実は異なるケースも存在する。
![308.png](https://qiita-image-store.s3.amazonaws.com/0/234814/7375639f-8eb8-3101-7786-6ce5467812e8.png)

統計情報を削除しつつ、上記のSQLを実行した結果を下図に示す。
クライアント統計の時間統計を確認すると、インデックスを使用しないほうが高速な結果となっている。
![309.png](https://qiita-image-store.s3.amazonaws.com/0/234814/4af1dbf5-9ce8-786d-73b1-f9fe9b05b30c.png)
![310.png](https://qiita-image-store.s3.amazonaws.com/0/234814/e981fff7-9ee3-74e8-5ddb-5e76ed2b8813.png)


#検証③　検索条件とインデックス適用について
- インデックスが適用されるかはオプティマイザの判断による（利用するインデックスを明示的に指定することは可能）	
- 基本的に、インデックスの先頭から指定されている条件があるかどうかが判断に大きな影響を与える	


***
###検証内容
国語と算数の点数で抽出すると、Index Seekとなり、インデックスが機能する。
![401.png](https://qiita-image-store.s3.amazonaws.com/0/234814/83955f91-b4e1-d76f-5c11-6390e193f7a5.png)

記述上の国語と算数の順番は、結果に影響しない。（オプティマイザがきちんと処理してくれる）
![402.png](https://qiita-image-store.s3.amazonaws.com/0/234814/07f4d7a0-329e-e180-3faa-62bdb154d96a.png)

国語の検索条件をはずすと、テーブル全件スキャンとなる。※インデックスを利用しても意味がないと判断される。
![403.png](https://qiita-image-store.s3.amazonaws.com/0/234814/eb9fb7d4-a5ea-032c-55fc-9c18918a8000.png)

範囲指定でもインデックスは機能する
![404.png](https://qiita-image-store.s3.amazonaws.com/0/234814/77a28466-18f2-3957-9eb7-f92faeb3380d.png)
★国語と算数で不等号の範囲指定も含めて処理される
![405.png](https://qiita-image-store.s3.amazonaws.com/0/234814/b73f1fc9-865f-9c64-61c7-46902edd8bde.png)

国語と理科を指定した場合、算数はないが、実際のところ国語だけである程度の絞込みが出来で、それが効率がいいと判定された場合、インデックスが利用される。
ここでは国語の77～80でIndex Seekでは4万件に絞り込まれ、その後、理科の条件も合致した1600件が、キー参照で実テーブルに照会される。
![406.png](https://qiita-image-store.s3.amazonaws.com/0/234814/071920df-bbbd-0b6e-eee0-16d31a79bbf4.png)
★Index Seekで使われてるのは国語だけ。
![407.png](https://qiita-image-store.s3.amazonaws.com/0/234814/b90afc04-606d-4d03-7ada-75b195b4c67f.png)

なお、ある程度、各点数の件数は分散されるようにしている。
そのため、国語だけで、1/100の絞込みが可能となっている。
※0～99点が約1万件ずつでトータル100万件。
![408.png](https://qiita-image-store.s3.amazonaws.com/0/234814/bd22844c-fa1a-8678-142f-8e4b11673df4.png)

国語を72点～80点に広げると、まず９万件⇒結果が約3500件、73点～80点の場合は、まず８万件⇒結果が約3100件となる。
この場合、72点～80点に広げるとインデックスが使用されなくなるが、前述のとおり、オプティマイザの予測がいつも正しいわけではなく、このケースではインデックスを使用したほうが処理が軽いため、WITH句でインデックスを指定すべきケースとなる。
![409.png](https://qiita-image-store.s3.amazonaws.com/0/234814/02146ef6-1839-1014-420e-c8374b2ae972.png)
![410.png](https://qiita-image-store.s3.amazonaws.com/0/234814/a766158d-06bd-a33c-3b24-399eee6bb481.png)



#検証④　カバーリングインデックスについて	
- インデックスは目次であり、実際のデータは目次からページを辿る
- 1件なら高速だが、結果の件数が1万件など、多くなるにつれて、ページを辿るコストが大きくなる
- そのため必要なデータもインデックスに格納すると早くなる⇒カバーリングインデックス
- 一方でカバーリングインデックスはストレージの容量を圧迫するデメリットがある（トレードオフ）
- <font color="Red">システムの処理速度が問題になっている場合に、データベース処理の高速化のメリットが大きければ、トレードオフを加味して判断する</font>
	
***
###検証内容	
前述のとおり、インデックスは[国語、算数、理科]に対して貼られている。
![501.png](https://qiita-image-store.s3.amazonaws.com/0/234814/611fe0aa-f5f7-d3ba-ceb7-22487ed04430.png)

もし結果として、理科の点数を表示する形であれば、インデックス内にデータが格納してあるため、処理がインデックスのみで完結する。
このため、索引として使用しないデータを敢えてインデックスとして格納することで、処理の高速化が実現できると分かる。
![502.png](https://qiita-image-store.s3.amazonaws.com/0/234814/29962f3f-9376-3d91-8bcc-45e8e391d633.png)

ここで違いを見るため、指定条件下の生徒の、理科と社会の平均点数をそれぞれ調べる。
![503.png](https://qiita-image-store.s3.amazonaws.com/0/234814/089d0cf3-ea51-35d2-a56d-13077b849a08.png)
理科と同様に社会をインデックスに盛り込めば、処理が高速化されることになる。
DB高速化のためよく使われるこの手法が、カバーリングインデックスである。
カバーリングインデックスはインデックスのサイズ増加のデメリットがあり、処理高速化のメリットと天秤に掛けて、都度判断する必要がある。

参考までに、今回のケースで下記のインデックスを新規に作成し、サイズ増加を見てみる。
　※本来であれば既存のインデックスに追加したほうが良いが、ここではディスク増加量の確認のため新規とした。
![504.png](https://qiita-image-store.s3.amazonaws.com/0/234814/f994510e-7c0a-ff64-280e-cc1fde26d7f0.png)
追加前
![505.png](https://qiita-image-store.s3.amazonaws.com/0/234814/5e338dfa-5222-d6a1-ee4a-ba6d01b43221.png)
追加後
![506.png](https://qiita-image-store.s3.amazonaws.com/0/234814/ae023fdf-a48d-7190-4b08-ea31019d63a4.png)
64MB⇒86MBに増加となった。
このとおり基本的にはカバーリングインデックスはトレードオフとなるため、
クエリの実行頻度、結果件数とその負荷具合など、様々な要素を加味して、
総合的に判断する必要がある。


#最後に
まとめではありません。
執筆者の理解も道半ばだからです。
今まで、索引を作るべきか、はたまたカバーリングインデックスも作るべきか、そもそも必要ないのか・・・と判断に迷うことがありました。そして迷ったときは、たいてい何もしませんでした。判断をする前提の知識がなかったからです。
もちろんそういった判断をするための知識としては、この記事だけでは十分ではありませんし、実際に自分でテストしていろいろと理解していく必要があります。この記事が、少しでもその一歩を踏み出す手助けになれば幸いです。