MySQLマルチカラムインデックスについて整理

きっかけ

「理論から学ぶデータベース実践入門 ~リレーショナルモデルによる効率的なSQL」の11章を読みました。

対象テーブル

users.sql

CREATE TABLE `users` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `age` tinyint unsigned NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `name_reverse` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `pref_id` tinyint unsigned NOT NULL,
  `gender` varchar(5) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `age_name_prefid` (`age`,`name`,`pref_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;;

INSERT INTO `users` (`id`, `age`, `name`, `name_reverse`, `pref_id`, `gender`) VALUES
(1, 20, 'Aaron', 'noraA', 9, 'men'),
(2, 20, 'Bob', 'boB', 34, 'men'),
(3, 20, 'Cammy', 'ymmaC', 24, 'women'),
(4, 40, 'Danny', 'ynnaD', 44, 'men'),
(5, 40, 'Eric', 'cirE', 21, 'men'),
(6, 40, 'Faker', 'rekaF', 15, 'men'),
(7, 27, 'George', 'egroeG', 45, 'men'),
(8, 27, 'Henry', 'yrneH', 33, 'women'),
(9, 60, 'Ivry', 'yrvI', 20, 'women'),
(10, 60, 'Johny', 'ynhoJ', 8, 'men'),
(11, 60, 'Karen', 'neraK', 4, 'women');

前置き

以降に書いている「Indexが効く」というのは今回のusersテーブルの場合のみです。
もちろん他のデータではカーディナリティや検索結果数などによって結果が変わります。

実践

Indexが効く場合

Indexの全てのカラムまたは左側のカラムを優先的に指定する

EXPLAIN SELECT * FROM `users` WHERE age=20 AND name='Aaron' AND pref_id=9;

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    ref age_name_prefid age_name_prefid 1024   const,const,const   1  100.00    NULL
EXPLAIN SELECT * FROM `users` WHERE age=20;

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    ref age_name_prefid age_name_prefid 1  const   3  100.00    NULL

Indexが効かない場合

Indexの項目を間抜けして指定する

EXPLAIN SELECT * FROM `users` WHERE age=20 order by pref_id;

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    ref age_name    age_name    1  const   3  100.00    Using filesort

Indexの一番左側のカラムを使用しない

EXPLAIN SELECT * FROM `users` WHERE name='Aaron' AND pref_id=9;

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    ALL   NULL    NULL    NULL    NULL    11 9.09  Using where

nippondanji.blogspot.com

なぜIndexが効かないのか

マルチカラムインデックスを設定したとき、Indexは以下のように並べられます。
まずcol1カラムの値でソートされた後、順にcol2,col3とソートされていくため、Indexにcol1からアクセスしないと適切な順序でレコードを取得することができません。
そのため間抜けでアクセスしたりcol1(一番左)項目を使用しないと、Indexが効きません。

col1 col2 col3
99 abc 1
99 xyz 1
100 aaa 1
100 aaa 2
100 abc 1
100 abc 2
100 abc 3
100 abc 4
100 xyz 1
101 abc 1
101 abc 2

カラムの順番

カーディナリティの高いカラムを先頭に(左に)指定する

カーディナリティの高いカラムを先頭に指定しておくことでIndexから取得するレコード数を少なくすることができます。
Explainのfilteredの値をみるとnameを先に指定しているIndexの方が高くなっている
※直感的に20歳以下のユーザ群からAaronを探すより、名前がAaronのユーザ群から20歳以下を探す方が総取得レコード数が少なそうじゃないですか?(DBのデータによりますが...)

ALTER TABLE `users` ADD KEY `age_name` (`age`,`name`);  
EXPLAIN SELECT * FROM `users` WHERE age<=20 AND name='Aaron';  

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    range   age_name    age_name    1023   NULL    1  10.00 Using index condition
ALTER TABLE `users` ADD KEY `name_age` (`name`,`age`);  
EXPLAIN SELECT * FROM `users` WHERE age<=20 AND name='Aaron';  

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    range   name_age    name_age    1023   NULL    1  100.00    Using index condition

scrapbox.io

OR検索 (番外編)

OR検索も複数カラムを使用しているのでマルチカラムインデックスを使うかもと思いましたが ORは各カラムにそれぞれIndexが必要で、それぞれの結果の集合和を結果として返します。( インデックスマージ)
key項目には複数のIndexが表示されており
Extraにはインデックスマージ時の特定のアルゴリズムが表示されています。

ALTER TABLE `users` ADD KEY `name` (`name`);
ALTER TABLE `users` ADD KEY `pref_id` (`pref_id`);
EXPLAIN SELECT * FROM `users` WHERE name='Aaron' OR pref_id=9;  

| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra|
1  SIMPLE  users   NULL    index_merge name,pref_id    name,pref_id    1022,1    NULL    2  100.00    Using union(name,pref_id); Using where

参考

dev.mysql.com

nippondanji.blogspot.com