InnoDB ( 2 )

August 13, 2018

クラスタインデックス

InnoDB はクラスタインデックスという構造となっており、データがインデックスのリーフノード上に格納されている。クラスタインデックスのキーとしては、通常は主キーもしくは一番始めの NULL を含まないユニークキーとなるが、それらが存在しない場合は暗黙的な 48 ビットの ROWID が主キーとして用いられるようになっている。なお、この ROWID はサーバ内でグローバルなカウンターから値が取得される。
クラスタインデックスの模式図を以下に示す。

f:id:shiro_kochi:2018××××××××:plain:w100:left

上の図は高さが 3 の B+ ツリーとなる。同じサイズのページ単位でデータが管理されており、根ノードから順にページをたどることで目的のデータへ行き当たる。末端のノードはリーフノードと呼ばれ、インデックスを検索した際に最終的に行き着く先となる。それ以外のノードはノンリーフノードと呼ばれる。

クラスタインデックスではデータがリーフノードのエントリ内に格納されており、インデックスを検索すればすぐに行データにたどり着けるようになっている。別途データが格納されている別の領域へアクセスする必要なく、主キーによる検索は効率が良い。またノード内の書くエントリは双方向リンクリストになっていて、ページ内の検索はリストをたどって行われる。InnoDB ではページ内でデータをソートしておらず、空きがあったら使うようになっているため、ページ内部では順番に並んでいないためである。

クラスタインデックスの利点は主キーの検索効率の良さとなる。リーフノードまで到達すれば、さらに追加でデータ領域にアクセスする必要はない。また、データは全て主キーの順に行スキャンをするのが得意となる。欠点としてはセカンダリインデックスを用いた場合で、セカンダリインデックスを使った検索の処理は、まずセカンダリインデックスから主キーの値が取得され、その後主キーからデータが取得されるという流れとなり、インデックスの検索が 2 回発生するため、独立したデータ領域に行データが格納されているときよりも効率が悪い。そのため、InnoDB ではカヴァリングインデックスにすることで、検索速度が改善するという傾向が見られる。

カヴァリングインデックスとは、セカンダリインデックスのリーフにアクセスするだけでクエリが解決できる (目的のデータが取得できる) ようなケースのことである (決して InnoDB の機能とかではない) 。

以下のようなテーブルを考える。

mysql> CREATE TABLE foo (id INT PRIMARY KEY AUTO_INCREMENT, type INT, price INT, data1 INT, data2 INT, data3 INT);  
Query OK, 0 rows affected (0.02 sec)  
  
mysql> INSERT INTO foo (type, price) VALUES (1, 100);  
Query OK, 1 row affected (0.00 sec)  
  
mysql> INSERT INTO foo (type, price) VALUES (2, 200);  
Query OK, 1 row affected (0.00 sec)  
  
mysql> INSERT INTO foo (type, price) VALUES (3, 300);  
Query OK, 1 row affected (0.01 sec)  
  
mysql> INSERT INTO foo (type, price) VALUES (4, 400);  
Query OK, 1 row affected (0.00 sec)  
  
mysql> INSERT INTO foo (type, price) VALUES (5, 500);  
Query OK, 1 row affected (0.01 sec)  
  
mysql> INSERT INTO foo (type, price) SELECT type, price FROM foo;  
Query OK, 5 rows affected (0.00 sec)  
Records: 5  Duplicates: 0  Warnings: 0  
  
mysql> INSERT INTO foo (type, price) SELECT type, price FROM foo;  
Query OK, 10 rows affected (0.00 sec)  
Records: 10  Duplicates: 0  Warnings: 0  
  
...  
  
mysql> INSERT INTO foo (type, price) SELECT type, price FROM foo;  
Query OK, 2621440 rows affected (12.89 sec)  
Records: 2621440  Duplicates: 0  Warnings: 0  
  
mysql> SELECT COUNT(*) FROM foo;  
+----------+  
| COUNT(*) |  
+----------+  
|  5242880 |  
+----------+  
1 row in set (1.37 sec)  

ここで、インデックスを使わず type ごとの price の合計を出してみる。

mysql> SELECT type, SUM(price) FROM foo GROUP BY type;  
+------+------------+  
| type | SUM(price) |  
+------+------------+  
|    1 |  104857600 |  
|    2 |  209715200 |  
|    3 |  314572800 |  
|    4 |  419430400 |  
|    5 |  524288000 |  
+------+------------+  
5 rows in set (3.32 sec)  

ここで、type のインデックスを作成して、同一のクエリを叩いてみる。

mysql> ALTER TABLE foo ADD INDEX (type);  
Query OK, 0 rows affected (12.58 sec)  
Records: 0  Duplicates: 0  Warnings: 0  
  
mysql> SELECT type, SUM(price) FROM foo GROUP BY type;  
+------+------------+  
| type | SUM(price) |  
+------+------------+  
|    1 |  104857600 |  
|    2 |  209715200 |  
|    3 |  314572800 |  
|    4 |  419430400 |  
|    5 |  524288000 |  
+------+------------+  
5 rows in set (3 min 29.51 sec)  

むしろ遅くなった。

mysql> EXPLAIN SELECT type, SUM(price) FROM foo GROUP BY type;  
+----+-------------+-------+------------+-------+---------------+------+---------+------+---------+----------+-------+  
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |  
+----+-------------+-------+------------+-------+---------------+------+---------+------+---------+----------+-------+  
|  1 | SIMPLE      | foo   | NULL       | index | type          | type | 5       | NULL | 5232592 |   100.00 | NULL  |  
+----+-------------+-------+------------+-------+---------------+------+---------+------+---------+----------+-------+  
1 row in set, 1 warning (0.00 sec)  

これはセカンダリインデックスの type が用いられ、type のリーフに price が含まれないが故に、type のリーフから主キーを取得 -> クラスターインデックスのリーフから price を取得しようとしたことにより、Disk I/O が激しくなったことにより発生している。
今欲しいのは type 及び price なので、インデックスを追加してあげれば良い。

mysql> ALTER TABLE foo ADD INDEX (type, price);  
Query OK, 0 rows affected (19.92 sec)  
Records: 0  Duplicates: 0  Warnings: 0  
  
mysql> SELECT type, SUM(price) FROM foo GROUP BY type;  
+------+------------+  
| type | SUM(price) |  
+------+------------+  
|    1 |  104857600 |  
|    2 |  209715200 |  
|    3 |  314572800 |  
|    4 |  419430400 |  
|    5 |  524288000 |  
+------+------------+  
5 rows in set (1.45 sec)  

セカンダリインデックスのリーフに price が含まれるようになったため、クラスターインデックスのリーフにアクセスする必要がなくなり、クエリが劇的に高速化していることがわかる。


 © 2023, Dealing with Ambiguity