- MySQL
MySQL インデックス確認|SHOW INDEXの見方と追加・削除
MySQLでテーブルのインデックスを確認するには、SHOW INDEX FROM テーブル名; を実行します。 インデックス1つにつき「構成する列の数だけ」行が返り、名前、列の順番、ユニークかどうか、値の種類数の推定(Cardinality)、オプティマイザから見えるか(Visible)などが一覧できます。
この記事では、SHOW INDEXの全15列の読み方を一覧表にまとめ、複合インデックス・前方一致インデックス・式インデックスの出力例を示します。さらに、CardinalityがANALYZE TABLEで変わる様子、インデックスの追加・削除・名前変更、INVISIBLEを使った試し外し、sysスキーマで不要・重複インデックスを洗い出すSQLまで、すべて実行した結果つきで順に見ていきます。
確認環境:Ubuntu 24.04、MySQL 8.0.46(InnoDB、mysqlコマンドラインクライアントで実行)。
SHOW INDEXの列の早見表
| 列名 | 意味 | 読み方のポイント |
|---|---|---|
| Table | テーブル名 | ― |
| Non_unique | 重複を許すか | 0=ユニーク(主キー・UNIQUE)、1=重複可 |
| Key_name | インデックス名 | 主キーは常に PRIMARY。同じ名前の行が複数あれば複合インデックス |
| Seq_in_index | インデックス内での列の順番(1始まり) | 複合インデックスの列順はここで読む |
| Column_name | 列名 | 式インデックスでは NULL(Expression列を見る) |
| Collation | 並び順 | A=昇順、D=降順、NULL=並びなし |
| Cardinality | ユニークな値の数の推定 | 推定値。ANALYZE TABLEで更新される |
| Sub_part | 前方何文字を索引にしたか | NULL=列全体、数値=先頭N文字だけ |
| Packed | キーの圧縮方法 | 圧縮なしは NULL |
| Null | NULLを含みうる列か | YES=NULL可、空文字=NOT NULL |
| Index_type | 索引の方式 | BTREE、FULLTEXT、HASH、RTREE |
| Comment | 列に出ない補足情報 | 無効化されているときの disabled など |
| Index_comment | 作成時にCOMMENTで付けた説明 | 用途のメモに使える |
| Visible | オプティマイザから見えるか | NO=不可視インデックス |
| Expression | 式インデックスの式 | 通常の列は NULL |
やりたいこと別のコマンドは次のとおりです。
| やりたいこと | コマンド |
|---|---|
| テーブルのインデックスを一覧する | SHOW INDEX FROM orders; |
| 縦に並べて読みやすくする | SHOW INDEX FROM orders\G |
| 特定のインデックスだけ見る | SHOW INDEX FROM orders WHERE Key_name = 'idx_x'; |
| 定義(DDL)で確認する | SHOW CREATE TABLE orders\G |
| 追加する | ALTER TABLE orders ADD INDEX idx_x (col); |
| 削除する | ALTER TABLE orders DROP INDEX idx_x; |
| 名前を変える | ALTER TABLE orders RENAME INDEX idx_x TO idx_y; |
| 試しに使われない状態にする | ALTER TABLE orders ALTER INDEX idx_x INVISIBLE; |
| 統計(Cardinality)を更新する | ANALYZE TABLE orders; |
| 重複インデックスを探す | SELECT * FROM sys.schema_redundant_indexes; |
| 使われていないインデックスを探す | SELECT * FROM sys.schema_unused_indexes; |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MySQL インデックス確認|SHOW INDEXの見方と追加・削除」
SHOW INDEXでインデックスを確認する
検証用のテーブル
主キー、ユニークインデックス、2列の複合インデックス、先頭10文字だけの前方一致インデックスを持つテーブルを用意します。
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(20) NOT NULL,
member_id INT NOT NULL,
status VARCHAR(10) NOT NULL,
note VARCHAR(200) NULL,
ordered_at DATETIME NOT NULL,
UNIQUE KEY uk_order_no (order_no),
KEY idx_member_ordered (member_id, ordered_at),
KEY idx_note (note(10))
) ENGINE=InnoDB;
SET SESSION cte_max_recursion_depth = 100000;
INSERT INTO orders (order_no, member_id, status, note, ordered_at)
WITH RECURSIVE seq (n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 50000)
SELECT CONCAT('ORD-', LPAD(n, 8, '0')), 1 + (n % 500),
ELT(1 + (n % 4), 'new', 'paid', 'shipped', 'done'),
IF(n % 5 = 0, NULL, CONCAT('memo ', n)),
'2026-01-01' + INTERVAL n MINUTE
FROM seq;
ANALYZE TABLE orders;
5万行で、member_id は500種類、status は4種類、note は5行に1行がNULLです。
実行結果(複合インデックスを含む出力例)
SHOW INDEX FROM orders;
+--------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| orders | 0 | PRIMARY | 1 | id | A | 50165 | NULL | NULL | | BTREE | | | YES | NULL |
| orders | 0 | uk_order_no | 1 | order_no | A | 49521 | NULL | NULL | | BTREE | | | YES | NULL |
| orders | 1 | idx_member_ordered | 1 | member_id | A | 494 | NULL | NULL | | BTREE | | | YES | NULL |
| orders | 1 | idx_member_ordered | 2 | ordered_at | A | 49286 | NULL | NULL | | BTREE | | | YES | NULL |
| orders | 1 | idx_note | 1 | note | A | 40680 | 10 | NULL | YES | BTREE | | | YES | NULL |
+--------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
5 rows in set
インデックスは4つですが、行は5つあります。idx_member_ordered が2行に分かれているのは、2列の複合インデックスだからです。 読み取れることを整理します。
| 行 | 読み取れること |
|---|---|
| PRIMARY | Non_unique=0。主キー。Key_nameは常に PRIMARY |
| uk_order_no | Non_unique=0。重複を許さないユニークインデックス |
| idx_member_ordered(Seq 1) | 複合インデックスの1列目が member_id |
| idx_member_ordered(Seq 2) | 2列目が ordered_at。Cardinalityは「1列目+2列目の組み合わせ」の推定 |
| idx_note | Sub_part=10。note の先頭10文字だけを索引にしている。Null=YESでNULLを含みうる |
横に長いときは \G で縦に並べる
列が多いので、ターミナルの幅に収まらないときは文末を \G にします。
SHOW INDEX FROM orders\G
*************************** 3. row ***************************
Table: orders
Non_unique: 1
Key_name: idx_member_ordered
Seq_in_index: 1
Column_name: member_id
Collation: A
Cardinality: 494
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
Visible: YES
Expression: NULL
(5行のうち3行目を載せています。)
絞り込み・別名・データベース指定
WHERE で列名を使って絞り込めます。SHOW KEYS と SHOW INDEXES は同じ結果を返す別名で、IN データベース名 で別のデータベースのテーブルも指定できます。
SHOW KEYS FROM orders IN t270 WHERE Non_unique = 0;
+--------+------------+-------------+--------------+-------------+-----------+-------------+ ...
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | ...
+--------+------------+-------------+--------------+-------------+-----------+-------------+ ...
| orders | 0 | PRIMARY | 1 | id | A | 50165 | ...
| orders | 0 | uk_order_no | 1 | order_no | A | 49521 | ...
+--------+------------+-------------+--------------+-------------+-----------+-------------+ ...
2 rows in set
(右側の列は省略しています。)
各列の読み方を詳しく見る
Non_uniqueとKey_name:ユニークかどうかと名前
Non_uniqueは「ユニークではない」の意味なので、0がユニーク、1が重複可です。 直感と逆に感じやすいところなので、「0=重複なし」と覚えておくと読み違えません。Key_nameが PRIMARY の行が主キーです。
Seq_in_index:複合インデックスの列順
複合インデックスは、Seq_in_indexが1の列から順に絞り込みに使われます。idx_member_ordered (member_id, ordered_at) は、member_id での検索や member_id と ordered_at の両方での検索に使えます。(ordered_at, member_id) という逆順のインデックスとは別物なので、列の順番まで確認することが大切です。
インデックスごとに列を1行にまとめて見たいときは、information_schema.STATISTICS を使います。
SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols,
MAX(NON_UNIQUE) AS non_unique
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 't270' AND TABLE_NAME = 'orders'
GROUP BY INDEX_NAME;
+--------------------+----------------------+------------+
| INDEX_NAME | cols | non_unique |
+--------------------+----------------------+------------+
| idx_member_ordered | member_id,ordered_at | 1 |
| idx_note | note | 1 |
| PRIMARY | id | 0 |
| uk_order_no | order_no | 0 |
+--------------------+----------------------+------------+
TABLE_NAME の条件を外せば、データベース内の全テーブルのインデックスを一度に一覧できます。
Cardinality:ユニークな値の数の推定
MySQLのリファレンスマニュアルでは、Cardinalityは「インデックス内のユニークな値の数の推定」と説明されています。整数で保持された統計から数えるため、小さなテーブルでも正確とは限らないこと、値が大きいほど結合でそのインデックスが使われやすいことも書かれています。
実際の値と比べてみます。
SELECT COUNT(*), COUNT(DISTINCT member_id),
COUNT(DISTINCT member_id, ordered_at), COUNT(DISTINCT LEFT(note,10))
FROM orders;
+----------+---------------------------+---------------------------------------+-------------------------------+
| COUNT(*) | COUNT(DISTINCT member_id) | COUNT(DISTINCT member_id, ordered_at) | COUNT(DISTINCT LEFT(note,10)) |
+----------+---------------------------+---------------------------------------+-------------------------------+
| 50000 | 500 | 50000 | 40000 |
+----------+---------------------------+---------------------------------------+-------------------------------+
| インデックス(列) | Cardinality(推定) | 実際の値 |
|---|---|---|
| PRIMARY(id) | 50165 | 50000 |
| uk_order_no(order_no) | 49521 | 50000 |
| idx_member_ordered の1列目(member_id) | 494 | 500 |
| idx_member_ordered の2列目まで | 49286 | 50000 |
| idx_note(note の先頭10文字) | 40680 | 40000 |
主キーでも50165と、実際の行数50000とは少しずれています。Cardinalityは「だいたいの桁と大小関係」を読むための数字です。行数に近いほど絞り込みやすい(選択性が高い)インデックス、行数に比べて極端に小さいほど絞り込みにくいインデックス、という見方をします。
Sub_part・Collation・Null
- Sub_part:
note(10)のように先頭の一部だけを索引にした場合、その文字数が入ります。NULLなら列全体です - Collation:
Aは昇順、Dは降順です。MySQL 8.0ではordered_at DESCのような降順インデックスを作れ、そのときDになります - Null:
YESならNULLを含みうる列です。NOT NULLの列は空文字で表示されます
VisibleとExpression:末尾の2列
2018年ごろの解説で見かける出力は Index_comment までの13列ですが、MySQL 8.0の出力は末尾に Visible と Expression の2列があり、15列です。式インデックス(関数インデックス)と降順インデックスを追加して、表示を確かめます。
ALTER TABLE orders ADD INDEX idx_status (status);
CREATE INDEX idx_ordered_at ON orders (ordered_at DESC);
ALTER TABLE orders ADD INDEX idx_note_upper ((UPPER(note)));
ALTER TABLE orders ADD INDEX idx_member (member_id), ALGORITHM=INPLACE, LOCK=NONE;
SHOW INDEX FROM orders;
追加した4つのインデックスの行を、要点の列に絞って載せます。
| Key_name | Seq_in_index | Column_name | Collation | Cardinality | Null | Visible | Expression |
+----------------+--------------+-------------+-----------+-------------+------+---------+---------------+
| idx_status | 1 | status | A | 3 | | YES | NULL |
| idx_ordered_at | 1 | ordered_at | D | 48888 | | YES | NULL |
| idx_member | 1 | member_id | A | 502 | | YES | NULL |
| idx_note_upper | 1 | NULL | A | 39627 | YES | YES | upper(`note`) |
idx_ordered_atは降順なので Collation が Didx_note_upperは式インデックスなので Column_name が NULL、Expression に式 が入るidx_statusの Cardinality は3(実際は4種類)。行数5万に対して極端に小さく、単独では絞り込みにくいインデックスだと読み取れます
SHOW EXTENDED INDEXで内部的な列も見る
EXTENDED を付けると、MySQLが内部で使う隠れた情報も表示されます。InnoDBのセカンダリインデックスには、末尾に主キーの列が付いていることがわかります。
SHOW EXTENDED INDEX FROM orders WHERE Key_name = 'idx_note';
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | ...
| orders | 1 | idx_note | 1 | note | A | 18480 | 10 | ...
| orders | 1 | idx_note | 2 | id | A | NULL | NULL | ...
(この出力は、あとの節で行を削除したあとに取ったものです。)
CardinalityはANALYZE TABLEで変わる
データを入れた直後は0のままだった
今回、テーブルを作って5万行をINSERTした直後にSHOW INDEXを実行すると、Cardinalityはすべて0でした。
| orders | 0 | PRIMARY | 1 | id | A | 0 | ...
| orders | 0 | uk_order_no | 1 | order_no | A | 0 | ...
| orders | 1 | idx_member_ordered | 1 | member_id | A | 0 | ...
ANALYZE TABLE を実行すると、値が入りました。
ANALYZE TABLE orders;
+-------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------+---------+----------+----------+
| t270.orders | analyze | status | OK |
+-------------+---------+----------+----------+
| orders | 0 | PRIMARY | 1 | id | A | 50165 | ...
| orders | 0 | uk_order_no | 1 | order_no | A | 49521 | ...
| orders | 1 | idx_member_ordered | 1 | member_id | A | 494 | ...
「Cardinalityが0や以前の値のまま」と感じたら、まずANALYZE TABLEを実行してから読み直します。
実行するたびに少し変わる
同じデータのまま、ANALYZE TABLEを2回続けて実行した結果です。
| インデックス(列) | 1回目 | 2回目 |
|---|---|---|
| PRIMARY(id) | 48888 | 50165 |
| uk_order_no(order_no) | 48888 | 50165 |
| idx_member(member_id) | 494 | 496 |
| idx_member_ordered(member_id) | 504 | 508 |
| idx_member_ordered(ordered_at まで) | 48888 | 49214 |
| idx_note(note) | 39627 | 38954 |
| idx_orders_status(status) | 3 | 3 |
データは1行も変えていませんが、値が動いています。推定値なので、計測のたびに少し揺れます。今回の環境の統計に関する設定は次のとおりでした。
SHOW VARIABLES WHERE Variable_name IN
('information_schema_stats_expiry','innodb_stats_persistent_sample_pages',
'innodb_stats_auto_recalc','innodb_stats_persistent');
+--------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------+-------+
| information_schema_stats_expiry | 86400 |
| innodb_stats_auto_recalc | ON |
| innodb_stats_persistent | ON |
| innodb_stats_persistent_sample_pages | 20 |
+--------------------------------------+-------+
大量に削除したあとの変化
member_id が251以上の行(全体の半分)を削除し、前後で比べます。
DELETE FROM orders WHERE member_id > 250;
SHOW INDEX FROM orders WHERE Key_name = 'idx_member'; -- Cardinality: 496
ANALYZE TABLE orders;
SHOW INDEX FROM orders WHERE Key_name = 'idx_member'; -- Cardinality: 245
SELECT COUNT(DISTINCT member_id) FROM orders; -- 250
削除の直後は496のままで、ANALYZE TABLEのあとに245(実際は250)へ更新されました。大量のINSERT・DELETE・データ移行のあとは、ANALYZE TABLEを実行してから実行計画やCardinalityを確認するのが確実です。
InnoDBの統計テーブルを直接見る
InnoDBが保持している統計は mysql.innodb_index_stats で見られます。複合インデックスの「何列目までで何種類か」が1行ずつ入っています。
SELECT index_name, stat_name, stat_value, stat_description
FROM mysql.innodb_index_stats
WHERE database_name='t270' AND table_name='orders' AND index_name='idx_member_ordered';
+--------------------+--------------+------------+-----------------------------------+
| index_name | stat_name | stat_value | stat_description |
+--------------------+--------------+------------+-----------------------------------+
| idx_member_ordered | n_diff_pfx01 | 244 | member_id |
| idx_member_ordered | n_diff_pfx02 | 25636 | member_id,ordered_at |
| idx_member_ordered | n_diff_pfx03 | 24912 | member_id,ordered_at,id |
| idx_member_ordered | n_leaf_pages | 96 | Number of leaf pages in the index |
| idx_member_ordered | size | 161 | Number of pages in the index |
+--------------------+--------------+------------+-----------------------------------+
size(ページ数)にページサイズを掛けると、インデックスごとのおおよそのサイズがわかります。
SELECT index_name, ROUND(stat_value * @@innodb_page_size / 1024) AS size_kb
FROM mysql.innodb_index_stats
WHERE database_name='t270' AND table_name='orders' AND stat_name='size';
+--------------------+---------+
| index_name | size_kb |
+--------------------+---------+
| PRIMARY | 3600 |
| idx_member | 1552 |
| idx_member_ordered | 2576 |
| idx_note | 2576 |
| idx_orders_status | 1552 |
| uk_order_no | 1552 |
+--------------------+---------+
インデックスを追加する(ADD INDEX・CREATE INDEX)
書き方は ALTER TABLE ... ADD INDEX と CREATE INDEX の2通りあり、どちらも同じ結果になります。先ほど実行した4つの文は、いずれも次のように完了しました。
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0
| 作りたいもの | 書き方 |
|---|---|
| 1列のインデックス | ALTER TABLE orders ADD INDEX idx_status (status); |
| 複合インデックス | ALTER TABLE orders ADD INDEX idx_status_ordered (status, ordered_at); |
| ユニークインデックス | ALTER TABLE orders ADD UNIQUE INDEX uk_x (col); |
| 降順インデックス | CREATE INDEX idx_ordered_at ON orders (ordered_at DESC); |
| 先頭N文字だけ | ALTER TABLE orders ADD INDEX idx_note (note(10)); |
| 式インデックス | ALTER TABLE orders ADD INDEX idx_note_upper ((UPPER(note))); |
| コメント付き・不可視で作る | ALTER TABLE orders ADD INDEX idx_ordered (ordered_at) COMMENT '期間検索用' INVISIBLE; |
| アルゴリズムとロックを明示 | ALTER TABLE orders ADD INDEX idx_member (member_id), ALGORITHM=INPLACE, LOCK=NONE; |
式インデックスは、式全体をもう一組のかっこで囲む点(((UPPER(note))))が書き方のポイントです。
ALGORITHM=INPLACE, LOCK=NONE を明示しておくと、その条件で実行できない場合はエラーで知らせてくれます。たとえば全文検索インデックスの追加で指定すると、次のように止まりました。
ALTER TABLE orders ADD FULLTEXT INDEX ft_note (note), ALGORITHM=INPLACE, LOCK=NONE;
ERROR 1846 (0A000): LOCK=NONE is not supported. Reason: Fulltext index creation requires a lock. Try LOCK=SHARED.
稼働中のテーブルへ追加するときに、意図しないロックを避けやすくなります。
追加でエラーになる例
同じ名前のインデックスを追加しようとした場合です。
ALTER TABLE orders ADD INDEX idx_status (status);
ERROR 1061 (42000): Duplicate key name 'idx_status'
重複する値がある列にユニークインデックスを追加しようとした場合です。
ALTER TABLE orders ADD UNIQUE INDEX uk_member (member_id);
ERROR 1062 (23000): Duplicate entry '2' for key 'orders.uk_member'
追加の前にSHOW INDEXで既存の名前と列を確かめておくと、どちらのエラーも事前に避けられます。
インデックスの名前を変える・削除する
RENAME INDEX
ALTER TABLE orders RENAME INDEX idx_status TO idx_orders_status;
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0
インデックスを作り直さずに名前だけが変わります。命名規則をそろえたいときに使えます。
DROP INDEX
こちらも2通りの書き方があります。
ALTER TABLE orders DROP INDEX idx_note_upper;
DROP INDEX idx_ordered_at ON orders;
存在しない名前を指定するとエラーになります。
ALTER TABLE orders DROP INDEX idx_nothing;
ERROR 1091 (42000): Can't DROP 'idx_nothing'; check that column/key exists
1つのALTER TABLEに、削除と追加をカンマで並べて書くこともできます。
ALTER TABLE orders DROP INDEX idx_member, ADD INDEX idx_status_ordered (status, ordered_at);
Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0
INVISIBLEで削除前に「試し外し」する
インデックスを削除してから「やはり必要だった」とわかると、大きなテーブルでは作り直しに時間がかかります。MySQL 8.0の不可視インデックス(INVISIBLE)を使うと、インデックスを残したまま、オプティマイザから見えない状態にできます。 問題があれば VISIBLE に戻すだけです。
不可視にして実行計画を比べる
member_id を含む2つのインデックスを不可視にします。
ALTER TABLE orders ALTER INDEX idx_member_ordered INVISIBLE;
ALTER TABLE orders ALTER INDEX idx_member INVISIBLE;
SHOW INDEX FROM orders WHERE Key_name IN ('idx_member_ordered','idx_member');
| Key_name | Seq_in_index | Column_name | Cardinality | Visible |
+--------------------+--------------+-------------+-------------+---------+
| idx_member_ordered | 1 | member_id | 494 | NO |
| idx_member_ordered | 2 | ordered_at | 49286 | NO |
| idx_member | 1 | member_id | 502 | NO |
(要点の列に絞っています。)Visibleが NO になりました。この状態で実行計画を見ます。
EXPLAIN SELECT * FROM orders WHERE member_id = 10;
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| 1 | SIMPLE | orders | NULL | ALL | NULL | NULL | NULL | NULL | 48888 | 0.19 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+-------+----------+-------------+
type が ALL(全行の読み取り)になり、key が NULL です。インデックスが使われなくなったことがわかります。VISIBLEに戻すと、元の実行計画に戻ります。
ALTER TABLE orders ALTER INDEX idx_member_ordered VISIBLE;
EXPLAIN SELECT * FROM orders WHERE member_id = 10;
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
| 1 | SIMPLE | orders | ref | idx_member_ordered | idx_member_ordered | 4 | const | 100 | 100.00 | NULL |
(要点の列に絞っています。)
不可視インデックスの決まりごと
| 項目 | 内容 | 確認方法 |
|---|---|---|
| 主キーは不可視にできない | ERROR 3522 (HY000): A primary key index cannot be invisible |
実行して確認 |
| 不可視でも更新は続く | 行の変更に合わせて保守され、ユニーク制約も働き続ける | リファレンスマニュアル「Invisible Indexes」 |
| ヒントで指定するとエラー | ERROR 1176 (42000): Key 'idx_member' doesn't exist in table 'orders' |
実行して確認(FORCE INDEX) |
| セッション単位で使わせる | SET SESSION optimizer_switch = 'use_invisible_indexes=on'; |
実行して確認 |
最後の use_invisible_indexes をonにすると、不可視のままでもそのセッションだけはインデックスが使われました。
SET SESSION optimizer_switch = 'use_invisible_indexes=on';
EXPLAIN SELECT member_id FROM orders FORCE INDEX (idx_member) WHERE member_id = 10;
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
| 1 | SIMPLE | orders | ref | idx_member | idx_member | 4 | const | 102 | Using index |
(要点の列に絞っています。)
新しいインデックスを INVISIBLE で作っておき、自分のセッションだけで効果を確かめてから VISIBLE にする、という逆向きの使い方もできます。
flowchart LR
A["削除候補を見つける"] --> B["ALTER INDEX ... INVISIBLE"]
B --> C{"しばらく様子を見る"}
C -->|遅くなるクエリが出た| D["ALTER INDEX ... VISIBLE で戻す"]
C -->|影響なし| E["DROP INDEX で削除"]
sysスキーマで不要・重複インデックスを洗い出す
schema_redundant_indexes:重複しているインデックス
sys.schema_redundant_indexes は、ほかのインデックスと重複している、またはほかのインデックスがあれば足りるインデックスを表示するビューです。
SELECT * FROM sys.schema_redundant_indexes WHERE table_schema = 't270'\G
*************************** 1. row ***************************
table_schema: t270
table_name: orders
redundant_index_name: idx_member
redundant_index_columns: member_id
redundant_index_non_unique: 1
dominant_index_name: idx_member_ordered
dominant_index_columns: member_id,ordered_at
dominant_index_non_unique: 1
subpart_exists: 0
sql_drop_index: ALTER TABLE `t270`.`orders` DROP INDEX `idx_member`
idx_member (member_id) は、idx_member_ordered (member_id, ordered_at) の先頭列と同じなので、重複として検出されました。sql_drop_index 列に、削除用のALTER TABLE文がそのまま入っています。 先ほど idx_member を削除して idx_status_ordered (status, ordered_at) を追加したあとにもう一度見ると、今度は idx_orders_status (status) が検出されました。
| redundant_index_name | redundant_index_columns | dominant_index_name | dominant_index_columns | sql_drop_index |
| idx_orders_status | status | idx_status_ordered | status,ordered_at | ALTER TABLE `t270`.`orders` DROP INDEX `idx_orders_status` |
schema_unused_indexes:使われていないインデックス
(この節の出力は、idx_member を削除する前に取ったものです。)
SELECT * FROM sys.schema_unused_indexes WHERE object_schema = 't270';
+---------------+-------------+--------------------+
| object_schema | object_name | index_name |
+---------------+-------------+--------------------+
| t270 | orders | idx_member_ordered |
| t270 | orders | idx_note |
| t270 | orders | idx_orders_status |
| t270 | orders | idx_member |
+---------------+-------------+--------------------+
このあと、idx_orders_status を使うクエリを1回実行します。
SELECT COUNT(*) FROM orders FORCE INDEX (idx_orders_status) WHERE status = 'paid';
SELECT * FROM sys.schema_unused_indexes WHERE object_schema = 't270';
+---------------+-------------+--------------------+
| object_schema | object_name | index_name |
+---------------+-------------+--------------------+
| t270 | orders | idx_member_ordered |
| t270 | orders | idx_note |
| t270 | orders | idx_member |
+---------------+-------------+--------------------+
一度使われた idx_orders_status が一覧から消えました。どのインデックスが何行読まれたかは sys.schema_index_statistics で見られます。
SELECT index_name, rows_selected, rows_inserted
FROM sys.schema_index_statistics
WHERE table_schema='t270' AND table_name='orders';
+--------------------+---------------+---------------+
| index_name | rows_selected | rows_inserted |
+--------------------+---------------+---------------+
| idx_orders_status | 12500 | 0 |
| uk_order_no | 1 | 0 |
| PRIMARY | 0 | 0 |
| idx_member_ordered | 0 | 0 |
| idx_note | 0 | 0 |
| idx_member | 0 | 0 |
+--------------------+---------------+---------------+
リファレンスマニュアルには、schema_unused_indexesは「サーバーが十分に長く稼働し、処理内容が普段の傾向を表すようになってから」役に立つ、とあります。起動直後や、月に一度の集計だけで使うインデックスがある場合は、一覧に出ていても必要なインデックスかもしれません。一覧は「削除の候補」として扱い、INVISIBLEでの試し外しを挟んでから削除する流れが安心です。
見直しの手順
| 手順 | 使うもの |
|---|---|
| 1. 現状を把握する | SHOW INDEX、information_schema.STATISTICS |
| 2. 統計を新しくする | ANALYZE TABLE |
| 3. 重複を探す | sys.schema_redundant_indexes |
| 4. 使われていないものを探す | sys.schema_unused_indexes、sys.schema_index_statistics |
| 5. 試しに外す | ALTER TABLE ... ALTER INDEX ... INVISIBLE |
| 6. 実行計画を確かめる | EXPLAIN |
| 7. 削除する(または戻す) | DROP INDEX / ALTER INDEX ... VISIBLE |
つまずきやすい点と確認ポイント
| 症状 | 原因 | 対処 |
|---|---|---|
| Cardinalityが0や以前の値のまま | 統計が更新されていない | ANALYZE TABLE を実行して読み直す |
| Cardinalityが行数と合わない | 推定値であるため | 桁と大小関係を読む。正確な値は COUNT(DISTINCT ...) |
| 同じKey_nameが複数行ある | 複合インデックス | Seq_in_indexの順に列を読む |
| Column_nameがNULL | 式インデックス | Expression列を見る |
| インデックスがあるのに使われない | Visible が NO | ALTER INDEX ... VISIBLE |
Duplicate key name |
同じ名前のインデックスが既にある | SHOW INDEXで名前を確かめる |
Can't DROP ...; check that column/key exists |
名前の指定違い | Key_nameをコピーして指定する |
| Non_uniqueの0と1を逆に読む | 「ユニークではない」が1 | 0=ユニーク と覚える |
よくある質問
Q. SHOW INDEXとSHOW CREATE TABLE、どちらで確認するのがよいですか?
A. 定義だけを手早く見るなら SHOW CREATE TABLE、Cardinality・Visible・列順など統計や状態まで見るなら SHOW INDEX です。データベース全体を横断して調べたいときは information_schema.STATISTICS をSELECTします。
Q. Cardinalityが低いインデックスは削除したほうがよいですか?
A. 低いだけでは決められません。今回の status(Cardinality 3)のような列は単独では絞り込みにくいものの、(status, ordered_at) のように複合インデックスの先頭列としては役に立つことがあります。sys.schema_index_statistics で読まれているかを確かめ、INVISIBLEで試してから判断します。
Q. インデックスを追加するとテーブルはロックされますか?
A. 今回は、通常のインデックスで ALGORITHM=INPLACE, LOCK=NONE を明示した追加が完了しました。全文検索インデックスでは同じ指定が ERROR 1846 で止まり、LOCK=SHARED を案内されました。この2つを書いておくと、ロックが必要な操作かどうかを実行前に見分けられます。
Q. 不可視インデックスは、更新の負荷も減りますか? A. 減りません。リファレンスマニュアルに、不可視でも行の変更に合わせて更新され続けるとあります。更新の負荷を減らすには、試し外しで影響がないことを確かめたうえでDROP INDEXします。
まとめ
- インデックスの確認は
SHOW INDEX FROM テーブル名;。横に長いときは\G、絞り込みはWHERE Key_name = ... - Non_uniqueは0がユニーク。同じKey_nameが複数行あれば複合インデックスで、Seq_in_indexが列順
- Cardinalityは推定値。実行するたびに少し変わり、大量の変更後はANALYZE TABLEで更新する
- VisibleとExpressionは8.0の出力で末尾にある2列。不可視インデックスと式インデックスの状態がわかる
- 追加はADD INDEX/CREATE INDEX、削除はDROP INDEX、名前変更はRENAME INDEX
- 削除の前にINVISIBLEで試し外し。問題があればVISIBLEに戻すだけ
- sys.schema_redundant_indexesとschema_unused_indexesで候補を洗い出す。削除用のSQLも列に入っている
参考・出典
- MySQL 8.0 Reference Manual「SHOW INDEX Statement」 https://dev.mysql.com/doc/refman/8.0/en/show-index.html
- MySQL 8.0 Reference Manual「Invisible Indexes」 https://dev.mysql.com/doc/refman/8.0/en/invisible-indexes.html
- MySQL 8.0 Reference Manual「The schema_unused_indexes and x$schema_unused_indexes Views」 https://dev.mysql.com/doc/refman/8.0/en/sys-schema-unused-indexes.html
- MySQL 8.0 Reference Manual「The schema_redundant_indexes and x$schema_flattened_keys Views」 https://dev.mysql.com/doc/refman/8.0/en/sys-schema-redundant-indexes.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「MySQL インデックス確認|SHOW INDEXの見方と追加・削除」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。