MySQLの使っていないインデックス、いきなり消さずに「不可視」で2週間様子を見てから消すようにした話
🙌 結論から
使っていないインデックスは、sys.schema_unused_indexes で候補を出して、いきなり消さずに INVISIBLE にしてから消すのが一番安全でした。
不可視にしたインデックスはオプティマイザから見えなくなるだけで、データはそのまま残ります👍
困ったら VISIBLE に戻すだけなので、インデックスを作り直す時間を待たなくていいんです!
ただし、sys.schema_unused_indexes の数字はサーバーを再起動するとリセットされるので、そこだけは気をつけたいところです(・_・;)
👀 インデックスが増えすぎていた
きっかけは、Laravel で運用している受注管理システムの orders テーブルです。
画面が増えるたびに「この検索が遅い」と言われてはインデックスを足してきた結果、気づけばインデックスだらけになっていました。。
読み込みは速くなっても、インデックスは INSERT や UPDATE のたびに更新されるので、書き込みのコストはじわじわ増えていきます。
しかも、仕様変更でもう使われていない検索条件のインデックスも残っていそうな状態でした(´・ω・`)
「たぶん使ってないけど、消して遅くなったら怖い」
これが正直な気持ちで、何年も手をつけられずにいたんですよね(´;ω;`)
💡 sys.schema_unused_indexesで候補を出す
MySQL には、使われていないインデックスを一覧で出してくれる sys.schema_unused_indexes というビューがあります。
中身は Performance Schema の table_io_waits_summary_by_index_usage を見ていて、一度も使われた記録がないインデックスが並びます。
主キー(PRIMARY)は最初から対象外です!
SELECT object_name, index_name
FROM sys.schema_unused_indexes
WHERE object_schema = 'app'
ORDER BY object_name;
これを本番に近い環境で流すと、候補がいくつも出てきました😆
ただ、公式ドキュメントにも書いてある通り、サーバーが十分長く動いていて、普段どおりの負荷を処理したあとでないと意味がないです。
カウンターはサーバーの起動からの累計なので、再起動した直後は全部「使われていない」ように見えてしまいます😭
私はこれを知らずに、メンテナンスで再起動した翌日に一覧を出して「こんなに使ってないの?」と驚きました。笑
もう1つ見落としがちなのが、月末だけ動くバッチです。
月次の集計でしか使わないインデックスは、月の途中に見ると「未使用」に出てきます🤔
✨ 消す前にINVISIBLEで様子を見る
候補が出たら、すぐに DROP せずに不可視にします🔥
ALTER TABLE orders ALTER INDEX orders_shipped_at_index INVISIBLE;
不可視のインデックスは、オプティマイザが実行計画を作るときに無視されます。
つまり「消したのと同じ状態」で本番の動きを確認できるわけです✨
もし遅くなる画面が出てきても、次の1行で元に戻せます。
ALTER TABLE orders ALTER INDEX orders_shipped_at_index VISIBLE;
消してしまったインデックスを大きなテーブルで作り直すのは時間がかかるので、戻すのが一瞬なのはかなりありがたいです・・!
逆に、気になるクエリだけで「インデックスがあったらどうなるか」を試したいときは、セッション単位で use_invisible_indexes をオンにします。
SET SESSION optimizer_switch = 'use_invisible_indexes=on';
EXPLAIN SELECT * FROM orders WHERE shipped_at >= '2026-09-01';
こうすると、不可視のままでもそのセッションだけはオプティマイザが考慮してくれるので、EXPLAIN の結果を見比べられます(^^)
なお、不可視にしても書き込み時の更新は止まりません。
書き込みのコストが減るのは、最後に DROP してからですね( .. )
🔧 私の手順とハマった点
今はこの流れで進めています。
- サーバーが再起動してから十分たっているか確認する(
SHOW GLOBAL STATUS LIKE 'Uptime';) sys.schema_unused_indexesで候補を出す- 月末バッチ・管理画面のCSV出力など、たまにしか動かない処理で使っていないか
grepで確認する - 候補を
INVISIBLEにして、月末をまたぐ2週間様子を見る - スロークエリログに新しい遅いクエリが出ていなければ、Laravel のマイグレーションで削除する
既存のインデックスを不可視にするマイグレーションは、素直に DB::statement() で書いています。
public function up(): void
{
DB::statement('ALTER TABLE orders ALTER INDEX orders_shipped_at_index INVISIBLE');
}
public function down(): void
{
DB::statement('ALTER TABLE orders ALTER INDEX orders_shipped_at_index VISIBLE');
}
様子見が終わったら、別のマイグレーションで $table->dropIndex('orders_shipped_at_index'); を実行します💪
ハマったのは、外部キーで使われているインデックスです。
未使用に出てきたので消そうとしたら、Cannot drop index '...': needed in a foreign key constraint というエラーで止まりました(T_T)
外部キーの列にはインデックスが必要なので、これは候補から外すしかありません(´・∀・)
あとは、UNIQUE のインデックスは読み込みで使われていなくても、重複を防ぐ役割があるので消してはいけません。
一覧に出てきても、UNIQUE かどうかは必ず確認するようにしています!
🔗 関連記事と使い分け
- MySQL の N+1 問題を解消した話: クエリの本数を減らす話。今回はその逆で、インデックスの本数を減らす話です
- paginate() を simplePaginate() にした話: スロークエリログの有効化はこちらで書いた設定をそのまま使っています。不可視にしている間の監視にも使えます
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
🙌 まとめ
使っていないインデックスを消すのが怖い人ほど、INVISIBLE を挟むだけでかなり気が楽になります。
候補は sys.schema_unused_indexes で出せますが、再起動の直後や月末バッチの前に見ると判断を間違えるので、そこだけは注意です。
私は「不可視で2週間 → 問題なければ削除」を自分ルールにしてから、インデックスの掃除にやっと手をつけられるようになりました(^o^)/
ちなみに執筆時点の MySQL は、2026年7月に出た 9.7.2 LTS・8.4.11 LTS・26.7.0(イノベーションリリース)が最新で、今回の機能はどれでも使えます😊
※ バージョン番号・料金・機能は今後も変わる可能性があります。最新情報は公式サイトをご確認ください👀