独立サイトのデータベース最適化:スロークエリログ、インデックス、テーブル構造のチューニング
1. WordPressのデータベースはなぜ遅くなるのか
WordPressはMySQLをデータベースとして使用しており、デフォルトのテーブル構造は小規模サイトなら問題なく動作するが、コンテンツが増えるとどんどん遅くなる。遅さの根源はサーバー設定ではなく、2つの設計上の問題にある。wp_postmetaテーブルはすべてのカスタムフィールドを1つのキーバリューテーブルに保存するため、1つの商品のパラメータを調べるのに複数回のJOINが必要になる。wp_optionsテーブルはすべての設定項目を混在させているため、管理画面を読み込むたびに全テーブルをスキャンすることになる。
1つのWordPressページは平均12〜20回データベースにクエリを実行し、その大部分はwp_postmetaの検索である。プラグインを10個インストールし、各プラグインがwp_postmetaにカスタムフィールドを書き込むと、このテーブルは数十万行に膨れ上がる可能性がある。この状態でmeta_queryを含むページリクエストが来ると、数十万行をスキャンすることになり、スロークエリが自然と発生する。
| よくあるスロークエリの発生源 | 関連テーブル | 典型的な症状 | 割合 |
|---|---|---|---|
| カスタムフィールドで記事を絞り込む | wp_postmeta | 商品絞り込みページの読み込みが3秒を超える | 約40% |
| 管理画面でプラグイン設定を読み込む | wp_options | 管理画面で保存するたびに数秒間白画面になる | 約20% |
| タグ・カテゴリのアーカイブページ | wp_term_relationships | 記事が増えるとアーカイブページが遅くなる | 約15% |
| コメントとユーザーテーブルのクエリ | wp_comments/wp_users | コメント一覧やユーザー管理が遅い | 約10% |
| プラグイン自体の複雑なクエリ | プラグインが独自に作成したテーブル | 特定の機能ページが特に遅い | 約15% |
データベースが遅くなると、ページキャッシュがある程度隠してくれる。ページキャッシュがヒットしているときはデータベースにクエリを実行しない。しかしキャッシュミス時(記事を公開した直後、キャッシュされていないページへのアクセス)には、スロークエリが露呈する。そのためデータベース最適化とキャッシュ戦略は合わせて確認する必要がある。詳細は独立サイトの3層キャッシュ戦略を参照。データベースがすでにCore Web Vitalsに影響を与えるほど遅くなっている場合、最適化の優先順位はCore Web Vitalsの最適化。
2. スロークエリログ:遅いSQLの見つけ方
データベースを最適化する第一歩はインデックスを追加することではなく、どのSQLが遅いのかを突き止めることです。目視で当て推量しても分かりません。あるクエリが遅いと思い込んでいても、実際は別のクエリかもしれません。MySQLにはスロークエリログが標準で備わっており、有効にすると閾値を超えたSQLがすべて記録されます。
MySQLの設定(my.cnfまたはmy.ini)に3行追加します。slow_query_log=1、long_query_time=1、slow_query_log_file=/var/log/mysql/slow.log。意味は、スローログを有効にし、1秒を超えたクエリを記録し、ログを指定ファイルに書き出すというものです。1日動かした後、mysqldumpslowやpt-query-digestでログをクエリ種別ごとに集計し、最も遅いSQLのTOP 10を洗い出します。
| ステップ | 操作 | ツール・コマンド | 目的 |
|---|---|---|---|
| 1. スローログを有効化 | my.cnfに3行の設定を追加 | MySQLを再起動して反映 | スローSQLを記録 |
| 2. 1日動かす | 通常どおりサイトに1日アクセス | —— | 実際のスロークエリを収集 |
| 3. 集計・分析 | SQLフィンガープリントで集計 | mysqldumpslow -s t | TOP 10を特定 |
| 4. 1件ずつEXPLAIN | 実行計画を確認 | EXPLAIN SQL文 | インデックスを使っているか判断 |
| 5. 最適化後に再計測 | 最適化前後の所要時間を比較 | SHOW PROFILEまたは計測 | 効果を確認 |
シナリオ例:商品絞り込みページが極端に遅いと気づき、スローログを見るとTOP 1のスロークエリはmeta_queryを含むSQLで、wp_postmetaテーブルの特定のカスタムフィールドを検索していました。EXPLAINで分析すると、type列はALL(全表スキャン)、rows列は12万行のスキャンを示しています。このSQLは毎回wp_postmetaテーブルを全件走査しているのです。この時点で、サーバーをアップグレードするのではなく、meta_keyフィールドにインデックスを追加すべきだと分かります。
スロークエリログは常時有効にしないでください。それ自体がIO負荷を伴います。1日動かしてデータを収集したら無効にするか、閾値を2秒に上げて最も遅いものだけを記録しましょう。日常的な監視にはデータベース性能監視ツール(Prometheus+Grafanaなど)を使います。詳細はサイト監視とアラートのチェックリスト。
三、インデックス最適化:どのフィールドにインデックスを追加すべきか
EXPLAINの結果で最も重要な2列はtypeとrowsです。type列がALLなら全表スキャンなので、インデックスの追加が必須です。refやeq_refならすでにインデックスを使っているので、手を加える必要はありません。rows列はMySQLが何行をスキャンすると見積もっているかを示し、数字が大きいほど危険です。1万行以内なら許容範囲ですが、10万行以上なら最適化すべきです。
WordPressデータベースで最もインデックスを追加すべきなのはwp_postmetaテーブルです。このテーブルはデフォルトでpost_idのインデックスしかありませんが、実際のクエリではmeta_keyやmeta_valueで絞り込むことがよくあります。(meta_key, meta_value)の複合インデックスを追加すると、カスタムフィールドで絞り込むクエリが10倍以上速くなります。注意すべきは複合インデックスであり、meta_key単体にインデックスを追加するのではないことです。meta_key単体では、クエリにmeta_valueの絞り込みが含まれる場合にテーブルへ戻る必要があります。
| テーブル | クエリの場面 | 推奨インデックス | 効果 |
|---|---|---|---|
| wp_postmeta | meta_key+meta_valueで絞り込み | (meta_key, meta_value(191)) | 10倍以上高速化 |
| wp_postmeta | post_idでmetaを検索 | post_idインデックスが既存 | 変更不要 |
| wp_options | option_nameで設定を検索 | option_nameインデックスあり | 変更不要 |
| wp_comments | post_idでコメントを検索 | comment_post_IDインデックスあり | 変更不要 |
| wp_posts | post_type+post_statusで検索 | 複合インデックスあり | 変更不要 |
| プラグインが独自に作成したテーブル | 日付/ステータスで絞り込み | EXPLAINを見て判断 | 必要に応じて追加 |
直感に反する判断:遅いクエリを見るとすぐインデックスを追加したくなりますが、インデックスは無料ではありません。各インデックスはディスク容量を消費し、INSERTやUPDATEのたびにMySQLはすべてのインデックスを更新します。wp_optionsのような書き込みが多く読み取りが少ないテーブル(管理画面で設定を保存するたびに書き込みが発生)では、やたらとインデックスを追加するとかえって書き込みが遅くなります。正しいやり方は、読み取りが多く書き込みが少ないテーブル(wp_postmeta、wp_posts)には積極的にインデックスを追加し、書き込みが多く読み取りが少ないテーブル(wp_options、wp_comments)には慎重に、追加せずに済むなら追加しないことです。
インデックスを追加する前に必ずstaging環境でテストしてください。本番DBで直接ALTER TABLEを実行すると、大きなテーブルでは数分間ロックされ、サイトが502になる可能性があります。本番環境でインデックスを追加する際は、pt-online-schema-changeやgh-ostなどのオンラインDDLツールを使えばロックされません。craterの海外向けサイトDBでの保守経験はこちらcraterの技術スタックと4つの自社開発プラグイン。
四、テーブル構造の最適化:wp_postmetaとwp_optionsの落とし穴
インデックスの追加では解決できない問題もあり、テーブル構造を変える必要があります。wp_postmetaのキーバリュー設計はまさにこの類の問題です。すべてのカスタムフィールドを1つのテーブルに保存するため、1つの商品の仕様パラメータを調べるのに何度もJOINが必要になります。商品数が数千、カスタムフィールドが数十になると、このテーブルは数十万行に膨れ上がり、インデックス最適化の効果は頭打ちになります。
ここで2つの方向性があります。1つはカスタムテーブルでpostmetaを置き換え、頻繁にクエリされるカスタムフィールドを別テーブルに分けてフィールド型ごとに保存し、クエリ時に対応するテーブルを直接参照する方法。もう1つはWordPressのwp_options autoload機能を使い、読み取りが多く書き込みが少ない設定項目を自動ロードに設定して、毎回optionsテーブルをクエリするのを避ける方法です。B2B独立サイトの場合、方向性1は商品パラメータが複雑なサイトに、方向性2はプラグインを多く導入しているサイトに適しています。
| 問題 | 症状 | 改善の方向 | リスク |
|---|---|---|---|
| wp_postmetaの肥大化 | 絞り込みページが遅い、テーブルが10万行超 | 頻繁に使うフィールドをカスタムテーブルに | コード変更、テストが必要 |
| wp_optionsの自動ロードが多すぎる | 管理画面が遅い、autoloadが1MB超 | 不要なプラグイン設定を削除 | 削除前にバックアップ |
| wp_postsの下書き/リビジョンが多すぎる | 記事一覧が遅い、不要なリビジョンが蓄積 | 自動リビジョンと下書きを削除 | 削除前に確認 |
| wp_commentmetaの肥大化 | コメント一覧が遅い | スパムコメントの自動保存を停止 | 影響は小さい |
| wp_usersテーブルが大きい | ユーザー管理が遅い | 非アクティブユーザーを削除 | 誤削除に注意 |
例:ある産業機器メーカーの公式サイトに50の製品があり、各製品に20のカスタムパラメータ(材質、出力、サイズ、精度など)があるとします。これらのパラメータがすべてwp_postmetaに保存されていると、製品詳細ページを1回表示するだけでwp_postmetaを20回JOINすることになります。正しい方法は、wp_product_specsというカスタムテーブルを作り、20のパラメータを列として1行にまとめることです。製品パラメータの取得はSELECT1回で済み、JOINは不要になります。開発工数はそれほど増えませんが、性能の向上は桁違いです。
テーブル構造を変更する前に必ずデータベースをバックアップしてください。バックアップと復元の手順はサイトのバックアップと復元演習をご覧ください。データベースがすでに攻撃を受けたことがある場合のクリーンアップと強化の手順はサイトがハッキングされた後の対処。
5. 最適化後の検証と保守
インデックスを追加したりテーブル構造を変更したら、それで終わりではありません。必ず次の3つを行います。1つ目は、EXPLAINで最適化前のスロークエリを再実行し、type列がALLからrefまたはeq_refに変わったことを確認します。2つ目は、最適化前後の実際の処理時間を比較し、スロークエリが本当に速くなったことを確認します。3つ目は、1週間観察してスロークエリログに新しいスロークエリが現れないか確認します。最適化はボトルネックを別の場所に移すことがよくあるからです。
| 検証項目 | 検証方法 | 合格基準 | 不合格時の対応 |
|---|---|---|---|
| EXPLAIN実行計画 | スロークエリをEXPLAINで再確認 | typeがALLでなくなり、rowsが1桁減る | インデックスが効いていないので、フィールド型を確認 |
| 実際のクエリ処理時間 | SQLを直接計測 | 1秒以上から100ミリ秒以内に短縮 | インデックスが適切でないので再分析 |
| スロークエリログ | 3日間実行してログを確認 | 元のスロークエリが消えるか大幅に減少 | 新しいスロークエリがあれば最適化を続行 |
| 書き込み性能 | INSERT/UPDATEの処理時間を計測 | 書き込みが目立って遅くなっていない | インデックスを追加しすぎたので冗長なインデックスを削除 |
| フロントページの表示時間 | Lighthouseのスコア | キャッシュミス時のLCP<1.5秒 | 他にもボトルネックがある |
データベースの最適化は一度きりの作業ではありません。コンテンツの増加、プラグインの更新、テーマの改修によって新しいスロークエリが発生します。月に一度はスロークエリログを分析し、TOP 10に変化がないか確認することをおすすめします。日々の保守においてデータベースの最適化は定番項目です。詳しくは公式サイト保守で毎月やる6つのことをご覧ください。独立系サイトの技術スタック全体の選定フレームワークは環形山テックスタック、PHPバージョンのアップグレードとデータベース設定の連携は公式サイト保守ガイド。
FAQ よくある質問
WordPressでMySQLのスロークエリログを有効にするには?
wp_postmetaテーブルはなぜこんなに遅いのか?
インデックスを追加するとサイトが遅くなりますか?
EXPLAINの結果はどう見ればいいですか?
本番DBでインデックスを追加するとテーブルがロックされますか?
データベースの最適化はどのくらいの頻度でやるべきですか?
最後に
データベース最適化で最も陥りやすい遠回りは、遅くなるとすぐサーバーをアップグレードすることです。1コア2GBから4コア8GBに上げて倍の費用を払っても、スロークエリは遅いままです。ボトルネックはCPUではなく、インデックスが使われていない全表走査だからです。正しい順序は常に、まずスローログを有効にしてどのSQLが遅いかを特定し、次にEXPLAINで実行計画を確認し、最後にインデックスを追加するかテーブル構造を変えるかを決めることです。
3ステップで解決します。まずスロークエリログを1日回して、mysqldumpslowでTOP 10を洗い出します。次に1件ずつEXPLAINして、type=ALLのものにインデックスを追加。wp_postmetaは(meta_key,meta_value)の複合インデックスを優先します。最後に最適化後の再テストで、遅いSQLが1秒から100ミリ秒以内に収まったことを確認します。この3ステップを終えると、サーバー設定は一切変えていないのに、ページ表示速度が数倍速くなっているはずです。
覚えておきたい一言。データベースが遅いときは、まずスロークエリログを確認しましょう。サーバーのスペックを上げるのは後回しです。遅いクエリの80%はインデックスが効かず全表走査になっているだけで、インデックスを1つ正しく追加するほうが、プランを1段階上げるより効果があります。
