WordPressの「再帰の呪縛」を解く:`wp_posts`の階層構造を物理的にハックせよ
WordPressの `post_parent` カラムを用いた階層構造は、小規模な構成なら問題ない。しかし、コンテンツが数千、数万と膨れ上がった瞬間、その設計は「パフォーマンスの爆弾」へと化す。
`get_children` や `WP_Query` による再帰的な親子探索は、内部で `wp_posts` テーブルをフルスキャンに近い形で叩くことになる。Depth(深さ)が深くなるほど、クエリのコストは指数関数的に増大する。
本稿では、WordPressのコアアーキテクチャを汚染せず、かつ極めて高速に階層パスを解決するための「Materialized Path(マテリアライズド・パス)」パターンの実装手法を伝授する。
—
なぜ再帰クエリが「悪」なのか
WordPressがデフォルトで行う `post_parent` を辿る処理は、データベースレベルで言えば「隣接リストモデル」だ。
SELECT FROM wp_posts WHERE post_parent = 123;
これ自体は高速だが、子の子、孫、ひ孫を取得しようとすると、PHP側で再帰関数を呼ぶか、複数のクエリを発行する必要がある。これはI/O負荷を増大させ、DBのスロークエリログを埋め尽くす原因となる。
我々が目指すべきは、「特定のIDの全子孫を一撃のクエリで取得する」設計だ。
—
解決策:マテリアライズド・パスの実装
`wp_postmeta` に `_hierarchy_path` というメタキーを追加し、そこに親から自分までのIDをスラッシュ区切りで保存する。
例:ID 10(親) -> 20(子) -> 30(孫) の場合
- ID 30のメタ値: `10/20/30/`
こうすることで、孫以下の全投稿は以下のクエリで一括取得可能になる。
SELECT post_id FROM wp_postmeta WHERE meta_key = ‘_hierarchy_path’ AND meta_value LIKE ’10/20/%’;
—
実装コード:堅牢な同期ロジック
`save_post` フックを用いて、投稿が保存されるたびにパスを再計算する。ここで重要なのは、「無限ループを避けること」と「トランザクション的な整合性」だ。
/
- 投稿保存時に階層パスを自動更新する
- 開発のリードとして、ここは必ず非同期または単一トランザクションで処理すべき
/
add_action(‘save_post’, ‘sync_hierarchy_path’, 10, 3);
function sync_hierarchy_path($post_id, $post, $update) {
// リビジョンや自動保存は無視
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return;
// 再帰呼び出しによる無限ループを防止するためのフラグ
remove_action(‘save_post’, ‘sync_hierarchy_path’, 10);
$parent_id = $post->post_parent;
$path = ($parent_id > 0)
? get_post_meta($parent_id, ‘_hierarchy_path’, true) . $post_id . ‘/’
: $post_id . ‘/’;
// パスを更新
update_post_meta($post_id, ‘_hierarchy_path’, $path);
// 子孫のパスも連鎖的に更新する必要がある場合(重要)
// 大規模な階層移動が発生する場合は、ここでAction Scheduler等を用いた非同期処理へ逃がすのが定石
update_descendants_paths($post_id, $path);
add_action(‘save_post’, ‘sync_hierarchy_path’, 10, 3);
}
function update_descendants_paths($parent_id, $parent_path) {
$children = get_posts([‘post_parent’ => $parent_id, ‘posts_per_page’ => -1]);
foreach ($children as $child) {
$new_path = $parent_path . $child->ID . ‘/’;
update_post_meta($child->ID, ‘_hierarchy_path’, $new_path);
update_descendants_paths($child->ID, $new_path);
}
}
—
プロダクション環境における注意点
この設計を実務で採用する際、以下の3点に注意せよ。
1. メタクエリのインデックス化: `wp_postmeta` テーブルはデフォルトで `meta_key` に対してインデックスが貼られているが、`meta_value` に対する `LIKE` 検索はインデックスが効きにくい。データ量が数万件を超える場合は、`wp_postmeta` ではなく、カスタムテーブル `wp_post_hierarchy` を作成し、`path` カラムにインデックスを貼ることを強く推奨する。
2. キャッシュの戦略: `wp_cache_set` を活用し、パス計算の結果をObject Cacheに載せること。DBアクセスを極力減らすのがパフォーマンス向上の鉄則だ。
3. データ不整合の防止: 階層の移動(親の変更)が発生した際、子孫のパスを即時更新するのは負荷が高い。高トラフィックなサイトでは、`Action Scheduler` を用いてバックグラウンドでパスを再構築する設計に切り替えろ。
結論
WordPressのコアは「柔軟性」を優先しているため、大規模データにおいて最適なパフォーマンスを維持するには、我々エンジニアが「物理構造」に手を入れる必要がある。
`post_parent` をそのまま信じるな。`wp_postmeta` をインデックスの海として活用せよ。この「マテリアライズド・パス」という武器を手にすれば、どんなに深い階層構造であっても、一撃でコンテンツを掌握できるはずだ。
コードレビューの際、もし再帰クエリを書いているメンバーがいたら、この記事を叩きつけてやってくれ。それが技術向上への最短ルートだ。