2026年7月29日
2026年7月29日
WordPressデータベースのインデックス最適化|EXPLAINで原因を特定してから安全に追加する手順
WordPressのデータベースが遅いとき、インデックスの追加が効くのは「特定のクエリが、インデックスを使えずに大量の行を読んでいる」場合に限られます。この記事では、スロークエリログとEXPLAINで遅いクエリを特定し、コアのアップグレードと衝突しにくい形でインデックスを追加・検証・撤回する手順を説明します。
先に知っておきたい前提
インデックスは万能ではありません。WordPressが遅い原因の多くは、自動読み込みされるオプションの肥大化、不要なリビジョンや期限切れトランジェントの蓄積、オブジェクトキャッシュが無いことによる同じクエリの繰り返しなど、インデックス以外のところにあります。まず「どのクエリが遅いのか」を特定し、そのクエリにインデックスが効くかを確かめてから手を入れるのが順番です。
また、インデックスは書き込みのたびに更新されるため、増やすほど INSERT や UPDATE が重くなり、ディスクも消費します。読み取りの速さと引き換えに書き込みコストを払うものだと考えてください。
作業前には、データベース全体のバックアップを取ってください。WP-CLIが使える環境なら次のコマンドで書き出せます。
wp db export backup-before-index.sql
手順1:遅いクエリを特定する
スロークエリログを使う(VPS・専用サーバー)
MySQL / MariaDB のスロークエリログを有効にすると、指定秒数より時間のかかったクエリが記録されます。設定ファイル(my.cnf など)に書くのが基本ですが、一時的に確認するだけなら次のように実行時に変更できます。再起動すると元に戻ります。
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
数時間から1日ほど記録したら、集計ツールで「合計時間が長いクエリ」を上から見ます。1回あたりは速くても回数が極端に多いクエリは、合計では最も重いことがあります。
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
Percona Toolkit が入っていれば pt-query-digest のほうが読みやすい集計を出せます。
Query Monitor を使う(共有サーバー)
サーバー設定を変えられない共有サーバーでは、Query Monitor プラグインが現実的です。管理画面やフロントの各ページで、実行されたクエリ、所要時間、呼び出し元のプラグインやテーマ、重複クエリを確認できます。遅いページを開き、時間のかかっているクエリと呼び出し元を控えておきます。確認が終わったら本番では無効化しておくと安心です。
手順2:EXPLAINで実行計画を確認する
遅いクエリが分かったら、先頭に EXPLAIN を付けて実行します。ここでは、カスタムフィールドの値で絞り込む典型的なクエリを例にします。テーブル接頭辞が wp_ 以外の場合は読み替えてください。
EXPLAIN
SELECT p.ID
FROM wp_posts p
INNER JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE p.post_type = 'event'
AND p.post_status = 'publish'
AND pm.meta_key = 'event_date'
AND pm.meta_value >= '20261001'
ORDER BY pm.meta_value ASC
LIMIT 20;
見るべき列は次の4つです。
- type:
ALLはテーブル全体の走査です。refやrangeならインデックスを使えています。 - key:実際に使われたインデックス名。NULL なら使われていません。
- rows:読むと見積もられた行数。結果の件数に比べて桁違いに多ければ改善の余地があります。
- Extra:
Using filesortやUsing temporaryは並べ替えや一時テーブルが発生している印です。
MySQL 8.0.18 以降では EXPLAIN ANALYZE で実測値も確認できます(2026年10月現在。MariaDB では ANALYZE 文が同様の役割です)。見積もりと実測が大きく違う場合は、ANALYZE TABLE wp_postmeta; で統計情報を更新してから再度確認します。
手順3:wp_postmeta のインデックスの実情を理解する
インデックス追加の相談が最も多いのが wp_postmeta です。現在の構成は次のコマンドで確認できます。
SHOW INDEX FROM wp_postmeta;
コアが定義しているのは、主キーの meta_id、post_id、meta_key の3つです。ここで押さえておきたい点が2つあります。
1つ目は、meta_key のインデックスが先頭191文字までの部分インデックスだということです。文字コード utf8mb4 では1文字最大4バイトになり、古いMySQLのインデックス長の上限に収めるために191文字に制限されています。キー名の照合には十分ですが、この事情を知らずにインデックスを作り直そうとすると、長さの指定でエラーになることがあります。
2つ目は、meta_value にはインデックスが無いことです。型が LONGTEXT のため、全体にインデックスは張れません。そのため「キーで絞ったあと、値は1行ずつ比較する」動きになり、同じ meta_key を持つ行が数十万件あると遅くなります。先頭の一部だけを対象にした複合インデックスは作れますが、効くのは文字列としての等価比較や前方一致、範囲比較に限られます。meta_query で type => 'NUMERIC' を指定すると、SQL上で CAST が掛かってインデックスを使えなくなる点にも注意が必要です。日付を 20261001 のような固定長の文字列で保存していれば、文字列比較のままで正しく並ぶため、インデックスが効きやすくなります。
手順4:インデックスを追加する
EXPLAINで「meta_key で絞ったあとに大量の行を読んでいる」と確認できた場合、meta_key と meta_value の先頭部分を組み合わせた複合インデックスが候補になります。
ALTER TABLE wp_postmeta
ADD INDEX lsc_meta_key_value (meta_key(191), meta_value(32)),
ALGORITHM=INPLACE, LOCK=NONE;
ポイントは次のとおりです。
- 名前に独自の接頭辞を付ける:
lsc_のように自分で付けたと分かる名前にすると、コアが将来同じ名前のインデックスを追加したときの衝突を避けやすく、後から見ても出どころが分かります。 - 値側の長さは短めにする:32文字は例です。実際に保存されている値の長さに合わせ、絞り込みに必要な分だけにします。長いほどインデックスが大きくなります。
- オンラインで実行する:InnoDB では
ALGORITHM=INPLACE, LOCK=NONEを指定すると、多くの場合テーブルを書き込みロックせずに作成できます。対応していない条件ではエラーで止まるので、意図せずロックされることはありません。それでも大きなテーブルではディスクI/Oが増えるため、アクセスの少ない時間帯に実行してください。
追加後は、手順2と同じEXPLAINを実行して key に新しいインデックス名が出ているか、rows が減ったかを確認します。速くならなければ、そのインデックスは不要なので削除します。
ALTER TABLE wp_postmeta DROP INDEX lsc_meta_key_value;
コアのアップグレードとの関係(dbDelta)
WordPressのメジャーアップデートでは、データベースの更新処理の中で dbDelta() がコアの定義とテーブルの実際の構造を比べ、足りない列やインデックスを追加します。dbDelta は定義に無い余分なインデックスを削除しないため、上記のように独自の名前で追加しただけのインデックスは、通常そのまま残ります。
一方で、次のような変更はアップグレード時の不具合につながる可能性があります。
- コアのインデックスを削除・改名した(dbDelta がコアの名前で作り直そうとし、同じ内容のインデックスが重複することがあります)
- コアのインデックスや主キーの定義そのものを変えた(将来コアがその定義を変更するときに ALTER が失敗するおそれがあります)
- 列の型を変えた
コアのインデックスには手を付けず、追加だけにとどめるのが安全です。主キー構成まで変更する「Index WP MySQL For Speed」のようなプラグインもありますが、使う場合は配布元の説明をよく読み、アップグレード前後の手順や元に戻す方法を把握したうえで、ステージング環境で試してください。
追加したインデックスは、名前・対象テーブル・追加理由・日付を運用メモに残しておきます。サイト移行やテーブルの作り直しで消えたときに、気づいて再作成できます。
インデックス以外に確認したいところ
EXPLAINの結果、インデックスでは解決しないと分かることもよくあります。その場合は次の点を確認します。
- 自動読み込みオプション:wp_options の自動読み込み対象は毎ページ読み込まれます。合計サイズを次のクエリで確認し、大きければ不要なプラグインの残骸を整理します。
SELECT autoload, COUNT(*) AS cnt, ROUND(SUM(LENGTH(option_value)) / 1024) AS kb
FROM wp_options
GROUP BY autoload;
- 不要データの蓄積:リビジョン、削除済みの投稿に紐づいたままのメタデータ、期限切れのトランジェント。
- WooCommerce の注文データ:WooCommerce には高速注文ストレージ(HPOS)や商品の検索用ルックアップテーブルがあり、wp_postmeta にインデックスを足すより効果的な場合があります。
- オブジェクトキャッシュ:Redis などの永続オブジェクトキャッシュを入れると、同じクエリの繰り返しそのものが減ります。
不要な既存インデックスを探したい場合、MySQL 5.7 以降の sys スキーマにある sys.schema_unused_indexes が参考になります。ただし集計はサーバー起動後の使用状況に基づくため、再起動直後の結果だけで削除を判断しないでください。
まとめ
- インデックスを足す前に、スロークエリログや Query Monitor で遅いクエリを特定し、EXPLAIN で「インデックスが使われていない」ことを確認する
- wp_postmeta の meta_key は191文字の部分インデックス、meta_value にはインデックスが無い。値で絞るクエリには先頭部分の複合インデックスが候補になる
- 追加は独自の接頭辞を付けた名前で、オンラインDDLを使い、アクセスの少ない時間帯に行う。追加後は EXPLAIN で効果を確かめ、効かなければ削除する
- コアのインデックスの削除・改名・定義変更は dbDelta によるアップグレード処理と衝突しうるため避け、追加だけにとどめる
- 作業前のバックアップと、追加したインデックスの記録を残す