ようこそ!WordPressの深遠なる内部構造の世界へ。
今日は、WordPress開発者が必ず一度は直面する「サイトが重くなる問題」の根本原因であり、データベース処理の要(かなめ)である`wp_posts`テーブルの複合インデックス設計とMySQLクエリプランナーの挙動について解説します。
他のプログラミング言語やフレームワークからWordPressに入ってきた方や、データベースの内部挙動を基礎からしっかり学びたい方に向けて、概念を図解的な例えとともに、分かりやすく丁寧に紐解いていきますね。
ここをしっかり理解してクリアできれば、単に「プラグインを作れる」段階を超えて、「大規模アクセスにも耐えうる頑丈なWordPressシステムを構築できる」本物のエンジニアへと成長できますよ。一緒に楽しく学んでいきましょう!
—
1. `wp_posts` の物理構造と標準インデックスの正体
まず、WordPressの心臓部である `wp_posts` テーブルが、データベース内でどのような構造になっているかを見てみましょう。
通常のブログ記事(`post`)も、固定ページ(`page`)、画像などのメディア(`attachment`)、さらにはカスタム投稿タイプも、すべてこの一つの `wp_posts` テーブルに格納されます。
データベースクライアントで `SHOW INDEX FROM wp_posts;` を実行すると、WordPressが標準で用意しているインデックス(索引)を確認できます。
— wp_posts テーブルのインデックス構造を確認するSQL
SHOW INDEX FROM wp_posts;
実行すると、以下のようなインデックス情報が返ってきます(一部抜粋)。
+———-+————+——————+————–+————-+
| Table | Non_unique | Key_name | Seq_in_index | Column_name |
+———-+————+——————+————–+————-+
| wp_posts | 0 | PRIMARY | 1 | ID |
| wp_posts | 1 | post_name | 1 | post_name |
| wp_posts | 1 | type_status_date | 1 | post_type |
| wp_posts | 1 | type_status_date | 2 | post_status |
| wp_posts | 1 | type_status_date | 3 | post_date |
| wp_posts | 1 | type_status_date | 4 | ID |
+———-+————+——————+————–+————-+
ここで最も注目してほしいのが、`type_status_date` という名前がついたインデックスです。
これは1つのカラム(列)だけではなく、複数のカラムを組み合わせた「複合インデックス(Composite Index)」になっています。
- `Seq_in_index`: 1 ➔ `post_type`
- `Seq_in_index`: 2 ➔ `post_status`
- `Seq_in_index`: 3 ➔ `post_date`
- `Seq_in_index`: 4 ➔ `ID`
WordPressのコア開発者は、なぜこの順番で複合インデックスを作成したのでしょうか? それを紐解く鍵が「カーディナリティ(Cardinality)」です。
—
2. インデックスの鍵を握る「カーディナリティ」とは?
「カーディナリティ(Cardinality)」という言葉、少し難しそうに聞こえますよね。
簡単に言うと、「その列に入っているデータのバリエーション(種類)の多さ」のことです。
例えば、10万件の記事データが入った `wp_posts` テーブルを想像してください。
| カラム名 | データの例 | 種類の多さ(バリエーション) | カーディナリティ |
| :— | :— | :— | :— |
| `post_status` | `publish`, `draft`, `trash` など | 数種類しかない | 低い (Low) |
| `post_type` | `post`, `page`, `attachment` など | 数種類〜数十種類 | 低い〜中程度 |
| `post_date` | `2026-03-30 10:15:30` など | ほぼ全件異なる | 非常に高い (High) |
| `ID` | `1`, `2`, `3` … | 完全に重複なし | 最高 (Unique) |
図解イメージ:図書館の本棚で本を探す
カーディナリティの概念を、図書館で特定の1冊の本を探すシーンで例えてみましょう。
- カーディナリティが低い(種類が少ない)列で探す(例: 性別や言語)
「『日本語で書かれた本』を持ってきて」と言われても、図書館の7割の本が該当してしまい、探す絞り込みとしてあまり役に立ちませんよね。
- カーディナリティが高い(種類が多い)列で探す(例: ISBNコードや発行日時)
「『2026年3月30日 10:15:30』に登録された本」と言われれば、一気に数冊まで絞り込めます。
じゃあ「カーディナリティが高い `post_date` だけにインデックスを貼ればいいのでは?」と思うかもしれません。ですが、ここにデータベース特有の面白いルールが存在します。
—
3. 複合インデックスの絶対ルール:「最左前置プレフィックスルール」
複合インデックス(`post_type`, `post_status`, `post_date`, `ID`)を使う場合、データベース(MySQL)は「左側に指定されたカラムから順番にしかインデックスを使えない」というルールを持っています。これを最左前置ルール(Leftmost Prefix Rule)と呼びます。
辞書(あいうえお順)をイメージしてください。
「苗字」➔「名前」の順番で並んでいる辞書で、「名前」だけから人物を検索するのは不可能ですよね。必ず「苗字」から探す必要があります。
WordPressでよく実行される典型的なSQLを見てみましょう。
— WP_Query が内部で発行する代表的なクエリ
SELECT FROM wp_posts
WHERE post_type = ‘post’
AND post_status = ‘publish’
ORDER BY post_date DESC
LIMIT 10;
このクエリに対して、標準の複合インデックス `(post_type, post_status, post_date, ID)` は完璧に噛み合います!
1. まず `post_type = ‘post’` で絞り込む(最左の1番目を使用)
2. 次に `post_status = ‘publish’` でさらに絞り込む(2番目を使用)
3. 絞り込まれた結果を `post_date` の順番で並べる(3番目を使用)
カーディナリティ単体で見れば `post_type` も `post_status` も低いですが、「2つを掛け合わせる(post + publish)」ことで、対象データを一気に全体の数%まで絞り込むことができるのです。
—
4. クエリプランナーの頭脳を覗く(`EXPLAIN` の読み解き)
MySQLの中には、SQLを受け取った時に「どのインデックスを使えば一番速く処理できるか?」を瞬時に計算する「クエリプランナー(Query Planner)」という頭脳が存在します。
クエリプランナーがどう判断しているかを確かめるために、SQL文の先頭に `EXPLAIN` をつけて実行してみましょう。
— クエリプランナーの実行計画を確認する
EXPLAIN SELECT FROM wp_posts
WHERE post_type = ‘post’
AND post_status = ‘publish’
ORDER BY post_date DESC
LIMIT 10;
実行結果の例
+—-+————-+———-+——-+——————+——————+———+—————+——+———————–+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+—-+————-+———-+——-+——————+——————+———+—————+——+———————–+
| 1 | SIMPLE | wp_posts | ref | type_status_date | type_status_date | 164 | const,const | 1250 | Backward index scan |
+—-+————-+———-+——-+——————+——————+———+—————+——+———————–+
注目すべき重要ポイントを解説しますね。
- `key`: 採用されたインデックスを示します。ここでは見事に `type_status_date` が選ばれています。
- `key_len`: インデックスの何バイト分を使ったかを示します(164バイト= `post_type` と `post_status` の2つのカラム分が絞り込みに使用されたことを表しています)。
- `rows`: 調査対象となった予測行数です。全データから大幅に絞り込まれていることがわかります。
- `Extra`: `Backward index scan` や `Using index condition` などが表示されます。もしここに `Using filesort` や `Using temporary` が出ている場合は、メモリ上での重い並び替えが発生しているサインなので注意が必要です!
—
5. 実践:カスタムクエリで直面するパフォーマンスの罠と解決コード
ここまでの知識を踏まえて、開発の現場でよくやってしまいがちな失敗例を見てみましょう。
陥りやすい罠:最左前置ルールを破壊するクエリ
例えば、「すべての投稿タイプから、最新の下書き(draft)を10件取得したい」という要件があったとします。
/
$args = array(
‘post_type’ => ‘any’, // 全ての投稿タイプが対象
‘post_status’ => ‘draft’,
‘posts_per_page’ => 10,
‘orderby’ => ‘date’,
‘order’ => ‘DESC’,
);
$query = new WP_Query( $args );
このとき発行されるSQLは以下のようになります。
SELECT FROM wp_posts
WHERE post_status = ‘draft’
ORDER BY post_date DESC
LIMIT 10;
お気づきでしょうか?
このクエリには `post_type` の条件がありません。
標準インデックス `(post_type, post_status, post_date, ID)` の一番左側(`post_type`)がスキップされてしまうため、MySQLのクエリプランナーはこのインデックスを効かせることができず、最悪の場合 フルテーブルスキャン(全件走査) を起こしてしまいます。
解決策:適切な複合インデックスを追加するカスタマイズ
もし、あなたの開発しているサイトやプラグインで「`post_status` と `post_date` だけでの検索」が頻繁に発生し、何十万件ものデータで低速化している場合、新しい複合インデックスを作成するのが劇的な解決策になります。
WordPressの標準作法に従って、安全にカスタムインデックスを追加するプラグインコードを作成してみましょう。
/
if ( ! defined( ‘ABSPATH’ ) ) {
exit; // 直接アクセスを防止
}
/
- プラグイン有効化時にインデックスを安全に追加する関数
/
function odi_add_custom_composite_index() {
global $wpdb;
$table_name = $wpdb->prefix . ‘posts’;
$index_name = ‘status_date_idx’;
// 1. 既にインデックスが存在するか確認
$index_exists = $wpdb->get_results(
$wpdb->prepare(
“SHOW INDEX FROM {$table_name} WHERE Key_name = %s”,
$index_name
)
);
// インデックスが存在しない場合のみ追加処理を実行
if ( empty( $index_exists ) ) {
/
- post_status と post_date の組み合わせインデックスを作成
- これにより post_type を指定しないステータス検索が劇的に高速化します
/
$sql = “ALTER TABLE {$table_name} ADD INDEX {$index_name} (post_status, post_date);”;
// dbDelta用のライブラリを読み込み(安全なテーブル変更用)
require_once( ABSPATH . ‘wp-admin/includes/upgrade.php’ );
// クエリを実行
$wpdb->query( $sql );
}
}
// プラグイン有効化フックに登録
register_activation_hook( __FILE__, ‘odi_add_custom_composite_index’ );
コードのポイント解説
1. 直接 `ALTER TABLE` を叩く前の重複チェック: 既に同じインデックスが存在する場合にエラーになるのを防ぎます。
2. `(post_status, post_date)` の順番: `WHERE post_status = ‘…’ ORDER BY post_date` というクエリに完璧にヒットする順番でインデックスを定義しています。
—
6. インデックス設計における注意点とまとめ
インデックスは貼れば貼るほど良いというものではありません。最後に、エンジニアとして覚えておくべきトレードオフ(相殺関係)をお伝えしますね。
1. 書き込み速度(INSERT / UPDATE / DELETE)の低下
インデックスを追加すると、記事を保存・更新するたびにデータベースはインデックス自体の更新処理も行う必要があります。貼りすぎると記事の保存動作が重くなります。
2. メモリの圧迫
インデックスは高速化のためにデータベースのメモリ(InnoDB Buffer Pool)に保持されます。不要なインデックスが増えると、貴重なRAMリソースを圧迫してしまいます。
本日のまとめ
- `wp_posts` の標準複合インデックスは `(post_type, post_status, post_date, ID)`。
- 最左前置ルールがあるため、`post_type` を条件に含めないと標準インデックスは効率よく使われない。
- 単体ではカーディナリティが低いカラムでも、組み合わせることで強力な絞り込み(高いカーディナリティ)を発揮する。
- クエリの遅さに悩んだら `EXPLAIN` を使って、クエリプランナーの選択を確認する。
データベースの内部構造とクエリプランナーの視点を持てるようになると、WordPressの開発が一気に面白くなってきますよね!
仕組みを理解して書かれたあなたのコードは、何十万件もの巨大なデータベースでも軽快に動き続けるはずです。ぜひ自信を持って、次のWordPress開発に活かしてみてくださいね!応援しています!