<SQL>複合インデックスの上手い張り方は？

#はじめに
[<SQL>テーブルカラムへの効果的なインデックス(索引)の張り方](https://qiita.com/ichi_zamurai/items/4f60f81913a1220b23b0)
この記事を書いていた時に、
今まで自分が調査した内容は、`単一インデックス`の張り方だけだったな〜と思い、
今度は**複合インデックス(コンポジット索引)**の効果的な張り方に着目してみたいと思います。

（基本的に、「インデックス」は、一般的なB-TREEインデックスで話しています。）


#複合インデックスとはそもそも？

単一カラムインデックスってのは、1つのカラムをキーとして索引を作り、検索効率化を図る目的でしたが、
要するに、
**テーブルの複数のカラムを組み合わせて1つのインデックスを張る**ってことですな。


適切な複合インデックスを定義するには、
**インデックスがどのように動作するか**だけでなく、

DBにどのようにデータ問い合わせするか
つまり、**WHERE句にどんなカラムの組合せで条件を指定するか、**

までも考えた方がいい。


#複合インデックス(コンポジット索引)は`順番`が命
##複合インデックスを張るときは、SQLの**評価順序**を意識すべし！！
**複合インデックスのカラムの設定順（評価させる順番）**や、
実際に**SQL文法の評価順を意識したクエリの記述**が、
複合インデックスを効かせるのに大事。

###カラム順序
例えば、
column1、column2という順番でインデックスを張る場合に、

1. column1を条件でスキャン
2. column2を条件でスキャン

っていう順序でしか索引スキャンは走らないので、
どっちのカラムを先に評価させるか、考える必要がある。

###SQLクエリ(SELECT)の評価順序
```sql
SELECT※⑦
	count(DISTINCT※⑧ t1.no)
FROM※①
	tbl1 t1
	LEFT JOIN※③
		tbl2 t2
	ON※②	t1.id = t2.t1_id
WHERE※④
	t1.delete_flg = 0
GROUP BY※⑤
	t1.id
HAVING※⑥ count(*) > 2
ORDER BY※⑨
	t1.id DESC
LIMIT※⑩ 1000
```

1. `FROM`
2. `ON`
3. `JOIN`
4. `WHERE`
5. `GROUP BY`
6. `HAVING`
7. `SELECT`
8. `DISTINCT`
9. `ORDER BY`
10. `TOP(LIMIT)`

というような順序で評価されるので、
**複合インデックスとして設定しているカラムの読み込ませる順番も考慮に入れる**必要がある。

例えば、
`column1` → `column２`の順番の複合インデックスの場合、

これは有効

```sql
SELECT * FROM table WHERE column1 = '検索条件' ORDER BY column2
```

次のは有効じゃない

```sql
SELECT * FROM table WHERE column2 = '検索条件' ORDER BY column1
```

#具体例で紐解きながら複合インデックスを理解する

家具の例で考えていきましょー。

| 商品No | 商品名 | 商品カテゴリ | 商品グループ | 価格 |
|:-:|:-:|:-:|:-:|:-:|
| 1 | すごいテーブル | テーブル | リビング | 30,000 |
| 2 | かっこいいイス | イス | ダイニング | 12,500 |
| 3 | かわいいソファ | イス | リビング | 5,6000 |
| 4 | えぐい枕 | 寝具 | ベッドルーム | 8,800 |
| 5 | 美しいライト | 照明 | リビング | 25,000 |

こんな商品テーブルがあった場合に、
３カラムで複合インデックスを張る例として、
1.`商品No`
2.`商品カテゴリ`
3.`商品グループ`
の順番に設定してみます。

```sql
CREATE INDEX index_name ON products (product_no, product_category, product_group);
```
```sql
ALTER TABLE products ADD INDEX index_name (product_no, product_category, product_group);
```

`※飽くまで複合インデックスの例です。
本来ならカーディナリティの低いカラムを後ろ側に設定すると、
データ重複が発生して、その分インデックスのデータ量も大きくなってしまい、
リソースの圧迫やデータ更新におけるパフォーマンスの悪化が懸念されるので注意が必要。`




##絞り込めるレコード数の多いカラムを複合インデックスの最初に設定すると良い◎
上記で言うと、
`商品No`ですね〜

理由として、
**カーディナリテイが高いカラム（絞り込めるレコード数が多い）**の場合に、
**足きりが多くできる**ので一気に対象レコードを絞り込めるメリットがあります。

商品Noは更新履歴とか持たない限り、
重複することはあまりないと思うので、一本釣りですな。


まあ、人間として、
`商品No`を覚えていて、それでWHERE句に指定して、検索実行なんてことは少ないと思いますが、
実際にAPIとかでクエリを投げるときは、自然と`商品No`も含めていると思います。


##検索クエリの条件でよく使われるカラムを複合インデックスに設定する
検索条件としてよく使用するカラムもインデックス登録しておくといいですね。

上記で言うと、
`商品カテゴリ` `商品グループ`
とかでしょうか。

カーディナリティ的には、
`商品カテゴリ` `>=` `商品グループ`
だと思うので、
（グループはせいぜい4つくらいですかね：Living Dining Kitchen Bathroom）
商品カテゴリを**2番目**の複合インデックス、商品グループを**3番目**の複合インデックスとして設定しますか。

（本来なら、もっとカーディナリティが欲しい。）

##等価比較(＝)するカラムを先に設定する
もし仮に、`価格`を複合インデックスに設定する場合、
`価格`は**範囲検索をする可能性が高い**ので、
`価格`よりも前に`商品カテゴリ`なんかの**等価比較するカラムをインデックス設定**しないと
インデックスがうまく効かないことも。

```sql
CREATE INDEX index_name ON products (product_price, product_category);// 良くない
```

`product_price`を先にインデックスに設定した場合に、

```sql
SELECT * FROM products WHERE product_price >= 10000 AND product_category = 'テーブル';
```
としてしまうと、`product_category`のインデックスが効かない恐れがある。

二分木走査する時に、
二個目のインデックスである商品カテゴリが、最初の**`価格`の条件が満たすかどうか判別できず**、
結局、商品カテゴリは、`product_price >= 10000`の条件下で、
**全走査されてしまい、商品カテゴリとしては、走査数が減らない事に**。

つまり、`商品価格`で単一インデックスを貼っているのと変わらない事態になるので、
こっちがイイね👍

```sql
CREATE INDEX index_name ON products (product_category, product_price);// ベター
```

##冗長なインデックスは避ける
例えば、テーブルに、`単一`+`複合`みたいに2つのインデックスを張るときに、
カラムが重複してたら、それは冗長なので、単一の方を消しちゃおう。

```sql
CREATE INDEX index_name1 ON products (product_no);
CREATE INDEX composite_index_name ON products (product_no, product_category, product_group);
```
というのも、
複合インデックスの方で、`product_no`の方のインデックスが効くから、単一のほうは不要。



#3つのカラムの複合インデックスの挙動
ざっくりですが、
3カラムに対する複合インデックスが張られたときは、

1. 1番目の列だけで検索する場合
2. 1番目と2番目の列で検索する場合
3. 全ての列で検索する場合

にインデックスが効果を発揮します。（詳細は後述）
※`WHERE`句で左から順に評価されるので、問題なくインデックスが効きます。

```sql
SELECT * FROM products WHERE product_no = 1;
SELECT * FROM products WHERE product_no = 1 AND product_category = 'テーブル';
SELECT * FROM products WHERE product_no = 1 AND product_category = 'テーブル' AND product_group = 'リビング';
```

##WHERE句に各カラムを設定した時の挙動
複合インデックスキーに設定されているカラムの全てが検索条件に無くても、
**キーの先頭から途中までのカラムが指定されていれば、インデックスが使える**

イメージはこんな感じです。

| 商品<br>No | 商品<br>カテゴリ | 商品<br>グループ | 複合インデックス利用<br>可不可 | 備考 |
|:-:|:-:|:-:|:--:|:--|
| ○ | ○ | ○ | 可 ||
| ○ | ○ | × | 可 ||
| ○ | × | × | 可 ||
| ○ | × | ○ | 可 |※商品Noのみ、商品グループは無視 |
| × | ○ | ○ | 可 |※索引スキップ・スキャン |
| × | ○ | × | 可 |※索引スキップ・スキャン |
| × | × | ○ | 可 |※索引スキップ・スキャン|
| × | × | × | 不可| |

####<注意事項>
- 使用するDBの種類（例：MySQL, Oracle, Postgre etc）によって、上記の挙動は変わります。
- 使用するDBのバージョンによっても上記の挙動は変わる
    - 例えば、`Oracle 8i`以前は複合インデックスの第1キーをWHEREに持たない検索はインデックスが使用不可だったが、`Oracle 9i`から、インデックススキップスキャンと呼ばれる高速化の仕組みによってインデックスを利用でき、パフォーマンスが向上する可能性がある
- スキップスキャンとは、インデックスをたどるときに、先頭列を無視する（スキップする）ことから、`INDEX SKIP SCAN`と呼ばれる
- 自分より前のカラムのカーディナリティによって、スキップスキャンかフルスキャンがされるか別れる
    - オプティマイザーの判断による

####ちなみに、WHERE句の書く順番はDBによってはインデックスを効かせてくれる。
DBによってはどちらの書き方でもよしなに対応してくれて、インデックスは使用されるが、
やはり、複合インデックスを張ったカラムの順番と同じ順番でWHERE句にも書いた方が、パフォーマンスが上がるそう。

```sql
#インデックスの定義 column1, column2
WHERE句
①WHERE column1 = 1 AND column2 = 2
②WHERE column2 = 2 AND column1 = 1
```


#まとめ

複合インデックスのクセがすごい。
シンプルに、クセでした。

以上、
ありがとうございました！