- Published on
MySQLクエリ最適化実践ガイド:EXPLAIN分析からインデックス設計・スロークエリチューニングまで
- Authors
- Name
- はじめに
- EXPLAIN分析完全ガイド
- インデックスの種類と特性
- 複合インデックス設計戦略
- クエリ書き換えパターン
- オプティマイザヒント
- スロークエリログの設定と分析
- InnoDBバッファプールチューニング
- MySQL 8.0 vs 8.4 オプティマイザ改善比較
- 失敗事例とリカバリ手順
- 本番環境最適化チェックリスト
- 運用上の注意事項
- 参考資料

はじめに
MySQLは世界で最も広く使われているリレーショナルデータベースの一つである。しかし、データが数千万件を超え、同時接続が増加すると、たった一行の非効率なクエリがサービス全体を麻痺させることがある。本記事では、EXPLAINコマンドによるクエリ実行計画の分析方法から、インデックス設計戦略、クエリ書き換えパターン、オプティマイザヒントの活用、スロークエリログ分析、そしてInnoDBバッファプールチューニングまで、MySQLクエリ最適化に必要なすべての内容を実践例とともに解説する。
本ガイドはMySQL基本構文に既に精通した開発者とDBAを対象としており、MySQL 8.0以上(特に8.4の改善点)を基準に作成している。
EXPLAIN分析完全ガイド
EXPLAINの基本的な使い方
EXPLAINはMySQLオプティマイザがクエリをどのように実行するかを事前に確認する必須ツールである。基本的な使い方はシンプルだ。
EXPLAIN SELECT u.name, o.total_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
AND o.created_at >= '2026-01-01';
出力結果は、クエリ内の各テーブルへのアクセス方法に対する行(row)として表示される。
typeカラムの解釈
typeカラムはテーブルアクセス方法を示し、パフォーマンスに最も直接的な影響を与える。良い順(速い順)から列挙すると以下の通りである。
| type値 | 説明 | パフォーマンス |
|---|---|---|
| const | 主キーまたはユニークインデックスで単一行検索 | 最高 |
| eq_ref | JOINで主キー/ユニークインデックスマッチング | 非常に良い |
| ref | 非ユニークインデックスで同値検索 | 良い |
| range | インデックス範囲スキャン(BETWEEN、IN等) | 普通 |
| index | 全インデックススキャン(インデックスツリー全体読み込み) | 悪い |
| ALL | 全テーブルスキャン(フルスキャン) | 最悪 |
-- const: 主キーで単一行検索
EXPLAIN SELECT * FROM users WHERE id = 1;
-- type: const, rows: 1
-- ref: 非ユニークインデックス検索
EXPLAIN SELECT * FROM orders WHERE user_id = 100;
-- type: ref(user_idにインデックスがある場合)
-- ALL: フルスキャン - 必ず改善が必要
EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- type: ALL(関数適用によりインデックス使用不可)
Extraカラムの解釈
Extraカラムにはオプティマイザの追加的な実行戦略が表示される。注意すべき値は以下の通りである。
| Extra値 | 意味 | 対処の必要性 |
|---|---|---|
| Using index | カバリングインデックスで解決 | 良好(維持) |
| Using where | WHERE句フィルタリング実行 | 普通 |
| Using filesort | ソートのための追加作業が必要 | 改善必要 |
| Using temporary | 一時テーブルの作成が必要 | 改善必要 |
| Using index condition | インデックスコンディションプッシュダウン適用 | 良好 |
EXPLAIN ANALYZEで実際の実行時間を確認
MySQL 8.0.18からEXPLAIN ANALYZEが使用できる。EXPLAINと異なり、クエリを実際に実行して実行時間と実際の行数を表示する。
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) as order_count
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2026-01-01'
GROUP BY u.id
HAVING order_count > 5
ORDER BY order_count DESC
LIMIT 10;
出力にはestimated rows、actual rows、actual timeなどが含まれ、オプティマイザの推定値と実際値の乖離を確認できる。推定行数(estimated)と実際行数(actual)の差が10倍以上あれば統計情報の更新が必要である。
-- 統計情報の手動更新
ANALYZE TABLE users;
ANALYZE TABLE orders;
インデックスの種類と特性
B-Treeインデックス
MySQL InnoDBのデフォルトインデックスタイプである。等値比較、範囲検索、ソート、そして最左プレフィックス(Leftmost Prefix)マッチングに最適化されている。
-- B-Treeインデックスの作成
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at);
-- このインデックスが効果的なクエリ例
SELECT * FROM orders WHERE user_id = 100 AND created_at >= '2026-01-01';
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC;
SELECT * FROM orders WHERE user_id IN (100, 200, 300);
Hashインデックス
MEMORYエンジンでのみ明示的に使用でき、InnoDBではAdaptive Hash Indexの形で自動管理される。等値(=)検索にのみ使用でき、範囲検索やソートには使用できない。
-- MEMORYエンジンテーブルでHashインデックスを使用
CREATE TABLE session_cache (
session_id VARCHAR(64) NOT NULL,
user_id INT NOT NULL,
data JSON,
INDEX USING HASH (session_id)
) ENGINE = MEMORY;
-- InnoDB Adaptive Hash Indexの状態確認
SHOW ENGINE INNODB STATUS\G
-- Adaptive Hash Indexセクションでhit rateを確認
Full-textインデックス
自然言語テキスト検索のための全文インデックスである。InnoDBではMySQL 5.6からサポートされ、MATCH ... AGAINST構文とともに使用する。
-- Full-textインデックスの作成
ALTER TABLE articles ADD FULLTEXT INDEX ft_idx_content (title, body);
-- 自然言語モード検索
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('MySQL パフォーマンス 最適化' IN NATURAL LANGUAGE MODE);
-- ブーリアンモード検索(AND、OR、NOT等)
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+MySQL +パフォーマンス -PostgreSQL' IN BOOLEAN MODE);
Spatialインデックス
空間データ(位置情報等)のためのR-Treeベースインデックスである。GEOMETRY、POINTなどのカラムに使用する。
-- Spatialインデックスの作成
CREATE TABLE locations (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
position POINT NOT NULL SRID 4326,
SPATIAL INDEX (position)
);
-- 半径内検索
SELECT name, ST_Distance_Sphere(position, ST_SRID(POINT(127.0276, 37.4979), 4326)) AS distance
FROM locations
WHERE ST_Distance_Sphere(position, ST_SRID(POINT(127.0276, 37.4979), 4326)) < 1000;
複合インデックス設計戦略
最左プレフィックスルール(Leftmost Prefix Rule)
複合インデックスは定義されたカラム順序で左から連続したプレフィックスのみを活用できる。これはMySQLインデックス設計で最も重要な原則である。
-- 複合インデックス: (a, b, c)
CREATE INDEX idx_abc ON orders (status, user_id, created_at);
-- インデックス活用可能なクエリ
SELECT * FROM orders WHERE status = 'completed'; -- (a) 使用
SELECT * FROM orders WHERE status = 'completed' AND user_id = 100; -- (a, b) 使用
SELECT * FROM orders WHERE status = 'completed' AND user_id = 100
AND created_at >= '2026-01-01'; -- (a, b, c) すべて使用
-- インデックス活用不可能なクエリ
SELECT * FROM orders WHERE user_id = 100; -- (b)のみ - インデックス未使用
SELECT * FROM orders WHERE user_id = 100 AND created_at >= '2026-01-01'; -- (b, c) - インデックス未使用
SELECT * FROM orders WHERE created_at >= '2026-01-01'; -- (c)のみ - インデックス未使用
カバリングインデックス(Covering Index)
クエリに必要なすべてのカラムがインデックスに含まれていれば、InnoDBはテーブルデータ(クラスタードインデックス)にアクセスせずセカンダリインデックスのみで結果を返すことができる。EXPLAINのExtraに「Using index」が表示される。
-- カバリングインデックスの設計
CREATE INDEX idx_covering ON orders (user_id, status, total_amount);
-- このクエリはインデックスのみで結果返却可能(Using index)
EXPLAIN SELECT user_id, status, total_amount
FROM orders
WHERE user_id = 100 AND status = 'completed';
-- このクエリはカバリングインデックス非適用(SELECT *によりテーブルデータアクセス必要)
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND status = 'completed';
インデックス設計実践パターン
インデックスのカラム順序を決定する際は以下の原則に従う。
- 等値(=)条件カラムを前に配置
- 範囲条件カラムを後ろに配置(範囲条件以降のカラムはインデックス活用不可)
- カーディナリティが高いカラムを優先配置(一般的な推奨だがクエリパターンの方が重要)
- ORDER BY / GROUP BY カラムを考慮
-- 悪い設計: 低カーディナリティカラムのみでインデックス
CREATE INDEX idx_bad ON orders (status); -- statusは5~10種類の値のみ
-- 良い設計: クエリパターンに合わせた複合インデックス
CREATE INDEX idx_good ON orders (user_id, status, created_at);
-- ソートまで考慮した設計
-- WHERE user_id = ? ORDER BY created_at DESC パターンが頻繁な場合
CREATE INDEX idx_sort ON orders (user_id, created_at DESC);
クエリ書き換えパターン
サブクエリからJOINへの変換
MySQLオプティマイザは相関サブクエリ(Correlated Subquery)を非効率的に処理することが多い。JOINに書き換えるとパフォーマンスが大幅に改善される。
-- 悪い例: 相関サブクエリ(各行ごとにサブクエリ実行)
SELECT u.name, u.email,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u
WHERE u.status = 'active';
-- 良い例: LEFT JOIN + GROUP BY
SELECT u.name, u.email, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
GROUP BY u.id, u.name, u.email;
-- または: 派生テーブル(Derived Table)の使用
SELECT u.name, u.email, COALESCE(oc.cnt, 0) AS order_count
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
) oc ON u.id = oc.user_id
WHERE u.status = 'active';
OR条件をUNIONに変換
OR条件はインデックスを非効率的に使用したり、フルテーブルスキャンを引き起こす可能性がある。
-- 悪い例: OR条件によるインデックス活用困難
SELECT * FROM products
WHERE category_id = 10 OR brand_id = 20;
-- 良い例: UNION ALLで分離(各条件に対してインデックス活用可能)
SELECT * FROM products WHERE category_id = 10
UNION ALL
SELECT * FROM products WHERE brand_id = 20 AND category_id != 10;
インデックスカラムへの関数適用回避
インデックスカラムに関数を適用するとインデックスが使用できなくなる。MySQL 8.0からは関数ベースインデックス(Expression Index)を作成できるが、可能であればクエリを書き換えるのが望ましい。
-- 悪い例: インデックスカラムに関数適用
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- 良い例: 範囲条件に書き換え
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
-- MySQL 8.0+: 関数ベースインデックス(Expression Index)
CREATE INDEX idx_email_lower ON users ((LOWER(email)));
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
暗黙の型変換の防止
カラム型と比較値の型が異なると、MySQLは暗黙の型変換を行い、インデックスが無視される。
-- 悪い例: phone_numberがVARCHARなのに数値で比較
SELECT * FROM users WHERE phone_number = 01012345678;
-- MySQLがphone_numberを数値に変換 -> インデックス未使用
-- 良い例: 型の一致
SELECT * FROM users WHERE phone_number = '01012345678';
オプティマイザヒント
インデックスヒント
-- USE INDEX: 特定インデックスの使用を推奨(オプティマイザが無視する可能性あり)
SELECT * FROM orders USE INDEX (idx_user_created)
WHERE user_id = 100 AND created_at >= '2026-01-01';
-- FORCE INDEX: 特定インデックスを強制使用(フルスキャンの代わりに必ずインデックス使用)
SELECT * FROM orders FORCE INDEX (idx_user_created)
WHERE user_id = 100 AND created_at >= '2026-01-01';
-- IGNORE INDEX: 特定インデックスを無視
SELECT * FROM orders IGNORE INDEX (idx_status)
WHERE status = 'completed' AND user_id = 100;
MySQL 8.0+ オプティマイザヒント構文
MySQL 8.0から導入された新しいヒント構文はコメント形式で記述する。
-- JOIN順序の固定
SELECT /*+ JOIN_ORDER(u, o) */
u.name, o.total_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active';
-- 特定テーブルのインデックス指定
SELECT /*+ INDEX(o idx_user_created) */
o.*
FROM orders o
WHERE o.user_id = 100;
-- Hash Joinの強制
SELECT /*+ HASH_JOIN(u, o) */
u.name, o.total_amount
FROM users u
JOIN orders o ON u.id = o.user_id;
-- 並列クエリ実行(MySQL 8.0.14+)
SELECT /*+ SET_VAR(innodb_parallel_read_threads=4) */
COUNT(*) FROM large_table;
optimizer_switchシステム変数
-- 現在の設定確認
SHOW VARIABLES LIKE 'optimizer_switch';
-- セッションレベルで特定の最適化を無効化
SET SESSION optimizer_switch = 'index_merge_intersection=off';
SET SESSION optimizer_switch = 'derived_merge=off';
SET SESSION optimizer_switch = 'batched_key_access=on';
-- MySQL 8.0.31+: ヒストグラムベースの最適化
ANALYZE TABLE orders UPDATE HISTOGRAM ON user_id, status WITH 100 BUCKETS;
スロークエリログの設定と分析
スロークエリログの設定
-- スロークエリログの有効化
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 1秒以上かかるクエリを記録
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- インデックス未使用クエリも記録
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
-- my.cnfでの恒久設定
-- [mysqld]
-- slow_query_log = 1
-- long_query_time = 1
-- log_queries_not_using_indexes = 1
-- slow_query_log_file = /var/log/mysql/slow-query.log
-- min_examined_row_limit = 1000
mysqldumpslowによる分析
# 最も遅いクエリTop 10
mysqldumpslow -s t -t 10 /var/log/mysql/slow-query.log
# 最も頻繁なスロークエリTop 10
mysqldumpslow -s c -t 10 /var/log/mysql/slow-query.log
# 特定パターンのフィルタリング
mysqldumpslow -s t -t 10 -g "orders" /var/log/mysql/slow-query.log
pt-query-digestによる詳細分析
Percona Toolkitのpt-query-digestはより強力な分析ツールである。
# スローログの分析
pt-query-digest /var/log/mysql/slow-query.log
# 特定時間範囲の分析
pt-query-digest --since="2026-03-10 00:00:00" --until="2026-03-11 00:00:00" \
/var/log/mysql/slow-query.log
# 結果をファイルに保存
pt-query-digest /var/log/mysql/slow-query.log > /tmp/slow-query-report.txt
出力にはクエリごとの実行回数、平均/最大実行時間、検査行数、返却行数などが含まれる。検査行数(Rows examined)と返却行数(Rows sent)の比率が100:1以上であればインデックス改善が必要である。
InnoDBバッファプールチューニング
バッファプールサイズの設定
InnoDBバッファプールはデータとインデックスをメモリにキャッシュする中核コンポーネントである。一般的に全体メモリの70~80%を割り当てる。
-- 現在のバッファプール状態の確認
SHOW VARIABLES LIKE 'innodb_buffer_pool%';
SHOW STATUS LIKE 'Innodb_buffer_pool%';
-- バッファプールヒット率の計算
SELECT
(1 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') /
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')
)) * 100 AS buffer_pool_hit_rate;
-- 99%以上が望ましい
バッファプールインスタンスの分離
同時実行性が高い環境ではバッファプールを複数のインスタンスに分離してmutex競合を軽減する。
# my.cnf設定例
[mysqld]
innodb_buffer_pool_size = 32G
innodb_buffer_pool_instances = 8 # バッファプールサイズ / インスタンス = 各4GB
innodb_buffer_pool_chunk_size = 1G # オンラインリサイズ単位
innodb_log_file_size = 4G
innodb_log_buffer_size = 64M
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
ウォームアップ(Buffer Pool Dump/Load)
MySQL再起動時にバッファプールが空になりパフォーマンスが低下するコールドスタート問題を防止する。
-- シャットダウン時にバッファプールダンプ、起動時にロード
SET GLOBAL innodb_buffer_pool_dump_at_shutdown = ON;
SET GLOBAL innodb_buffer_pool_load_at_startup = ON;
-- 手動ダンプ/ロード
SET GLOBAL innodb_buffer_pool_dump_now = ON;
SET GLOBAL innodb_buffer_pool_load_now = ON;
-- ロード進捗状態の確認
SHOW STATUS LIKE 'Innodb_buffer_pool_load_status';
MySQL 8.0 vs 8.4 オプティマイザ改善比較
| 機能 | MySQL 8.0 | MySQL 8.4 |
|---|---|---|
| EXPLAIN FORMAT | TRADITIONAL, JSON, TREE | TRADITIONAL, JSON, TREE(改善) |
| Hash Join | 8.0.18からサポート | パフォーマンスとメモリ管理の改善 |
| Window Functions | 基本サポート | 実行計画最適化の改善 |
| Derived Table Merge | 一部サポート | より広範囲のサポート |
| Invisible Index | サポート | サポート(管理機能の改善) |
| Functional Index | サポート | サポート(Expression Index改善) |
| Histogram Statistics | サポート | 自動更新機能の強化 |
| Parallel Query | 限定的 | InnoDB並列読み取りの拡大 |
| Cost Model | デフォルトコストモデル | コスト定数の再調整、SSD対応 |
| Subquery Optimization | 基本セミジョイン | アンチジョインおよびセミジョイン戦略の拡大 |
| EXPLAIN ANALYZE | 8.0.18で導入 | 出力形式の改善、メモリ使用量表示 |
-- MySQL 8.4: EXPLAIN改善の例
EXPLAIN FORMAT=TREE
SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
-- Invisible Indexの活用(インデックス削除前のテスト)
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE;
-- パフォーマンステスト後、問題なければ削除
-- DROP INDEX idx_status ON orders;
-- 問題が発生した場合は再度有効化
ALTER TABLE orders ALTER INDEX idx_status VISIBLE;
失敗事例とリカバリ手順
事例1: 暗黙の型変換によるインデックス未使用
-- 問題状況: VARCHARカラムに対する数値比較
-- account_noカラムがVARCHAR(20)だがアプリケーションから数値が渡される
EXPLAIN SELECT * FROM accounts WHERE account_no = 123456;
-- type: ALL(全テーブルスキャン!)
-- 原因: MySQLがaccount_noカラムを数値に変換するためインデックス使用不可
-- 解決: 型を一致させる
EXPLAIN SELECT * FROM accounts WHERE account_no = '123456';
-- type: ref(インデックス使用)
事例2: カーディナリティ不足によるインデックススキップ
-- 問題状況: インデックスがあるがオプティマイザがフルスキャンを選択
-- statusカラムにインデックスがあるが'active'が全体の90%を占める
EXPLAIN SELECT * FROM users WHERE status = 'active';
-- type: ALL(オプティマイザがフルスキャンの方が効率的と判断)
-- 原因: カーディナリティが低くインデックス効率が悪い
-- 解決1: 他の条件と複合インデックスを構成
CREATE INDEX idx_status_created ON users (status, created_at);
-- 解決2: 特定の値に対してのみインデックスが有用な場合
-- MySQLはPartial Indexを直接サポートしないため、クエリパターンの変更を検討
事例3: 誤った複合インデックスの順序
-- 問題状況: インデックスを(created_at, user_id)で作成
-- 頻繁なクエリ: WHERE user_id = ? AND created_at >= ?
EXPLAIN SELECT * FROM orders
WHERE user_id = 100 AND created_at >= '2026-01-01';
-- インデックスを非効率的に使用(created_atで先にrange scan)
-- 解決: 等値条件カラムを先に配置
DROP INDEX idx_created_user ON orders;
CREATE INDEX idx_user_created ON orders (user_id, created_at);
-- type: range(効率的な範囲スキャン)
事例4: 大量DELETE後のインデックス断片化
-- 問題状況: 500万件DELETE後のクエリパフォーマンス低下
-- 原因: インデックスページに空きスペースが多い(断片化)
-- 診断
SELECT
TABLE_NAME,
INDEX_LENGTH,
DATA_LENGTH,
DATA_FREE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'orders';
-- 解決: インデックスの再構築
ALTER TABLE orders ENGINE=InnoDB; -- テーブル + インデックスの再構築
-- または
OPTIMIZE TABLE orders;
本番環境最適化チェックリスト
インデックス点検
- すべての外部キー(Foreign Key)カラムにインデックスがあるか確認
- 主要クエリのEXPLAIN結果でtypeがALLのものがないか検証
- 未使用インデックスの特定と削除
- 重複インデックス(Duplicate Index)の削除
- 複合インデックスのカラム順序がクエリパターンに合っているか確認
-- 未使用インデックスの検索
SELECT
s.TABLE_SCHEMA,
s.TABLE_NAME,
s.INDEX_NAME,
s.COLUMN_NAME
FROM INFORMATION_SCHEMA.STATISTICS s
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage p
ON s.TABLE_SCHEMA = p.OBJECT_SCHEMA
AND s.TABLE_NAME = p.OBJECT_NAME
AND s.INDEX_NAME = p.INDEX_NAME
WHERE p.COUNT_STAR = 0
AND s.TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
AND s.INDEX_NAME != 'PRIMARY'
ORDER BY s.TABLE_SCHEMA, s.TABLE_NAME;
-- 重複インデックスの検索(sysスキーマ活用)
SELECT * FROM sys.schema_redundant_indexes;
クエリ点検
- スロークエリログの有効化と定期的な分析
- インデックスカラムに関数を適用しているクエリがないか確認
- 暗黙の型変換が発生するクエリがないか確認
- SELECT * の代わりに必要なカラムのみを選択
- N+1クエリパターンの排除
サーバー設定点検
- innodb_buffer_pool_sizeが全体メモリの70~80%であるか確認
- innodb_buffer_pool_instancesが適切か確認(最低8、バッファプール1GBあたり1インスタンス)
- innodb_log_file_sizeが十分か確認(1~4GB推奨)
- innodb_flush_log_at_trx_commit値の確認(1: 安全、2: 妥協)
- innodb_buffer_pool_dump_at_shutdown/load_at_startupの有効化
モニタリング
- バッファプールヒット率99%以上の維持
- スロークエリ発生頻度のモニタリング
- インデックス使用率の定期点検
- テーブル統計情報の自動更新確認
- ロック待ち(Lock Wait)およびデッドロック(Deadlock)のモニタリング
運用上の注意事項
大容量テーブルのインデックス変更時の注意点
MySQL 8.0ではほとんどのALTER TABLE ... ADD INDEXがオンライン(InstantまたはIn-place)で処理されるが、本番環境では必ず事前テストを実施する必要がある。
-- インデックス追加時のロックモード確認
ALTER TABLE orders ADD INDEX idx_new (col1, col2), ALGORITHM=INPLACE, LOCK=NONE;
-- pt-online-schema-changeの使用(大容量テーブルに推奨)
-- トリガーで元テーブルを同期しながらスキーマ変更を実行
pt-online-schema-change \
--alter "ADD INDEX idx_new (col1, col2)" \
--execute \
D=mydb,t=orders
クエリキャッシュに関する注意
MySQL 8.0からクエリキャッシュ(Query Cache)は完全に廃止された。アプリケーションレベルのキャッシュ(Redis、Memcached等)で代替する必要がある。
ヒストグラム統計の活用
MySQL 8.0で導入されたヒストグラムはカラムの値分布をオプティマイザに提供し、より良い実行計画の選択を支援する。
-- ヒストグラムの作成
ANALYZE TABLE orders UPDATE HISTOGRAM ON status WITH 100 BUCKETS;
ANALYZE TABLE orders UPDATE HISTOGRAM ON total_amount WITH 254 BUCKETS;
-- ヒストグラムの確認
SELECT
SCHEMA_NAME,
TABLE_NAME,
COLUMN_NAME,
JSON_EXTRACT(HISTOGRAM, '$.histogram-type') AS histogram_type,
JSON_EXTRACT(HISTOGRAM, '$.number-of-buckets-specified') AS buckets
FROM INFORMATION_SCHEMA.COLUMN_STATISTICS;
-- ヒストグラムの削除
ANALYZE TABLE orders DROP HISTOGRAM ON status;