【実務・中級編】wp_commentsテーブルの階層構造を再帰クエリなしで取得するパス列挙モデルの導入 – WordPress 内部コア・データベース構造とパフォーマンス最適化解析バイブル

貴様のコメント取得コードが遅いのは、「隣接リストモデル」の限界を理解していないからだ

WordPressのコアにおいて、`wp_comments` テーブルの階層構造は古き良き「隣接リストモデル(Adjacency List Model)」、すなわち `comment_parent` カラムによる自己参照で管理されている。

小規模なブログならそれでいい。だが、数千件のコメントがぶら下がる大規模メディアや、コメント欄を掲示板やタイムラインとして転用しているシステム設計において、再帰的な関数(`get_comments` をループ内で回す、あるいはPHP側でネスト構造を構築する)を走らせるのは、パフォーマンス上の自殺行為だ。

計算量は $O(N^2)$ に近づき、データベースとの往復(Round Trip)がフロントエンドのレスポンスを致命的に損なう。プロなら、Path Enumeration(パス列挙モデル)を導入し、単一のクエリで階層構造を「一撃」で引き抜く設計を選択すべきだ。

今回は、WordPressのメタデータに頼らず、物理構造を拡張して「物理的なパス」を保持し、再帰クエリなしでツリー全体を高速取得する極限の手法を伝授する。

—

1. 「隣接リスト」という負債、そして「パス列挙」という解

標準の `comment_parent` を使ったクエリでは、特定のコメントに連なる子・孫・ひ孫をすべて取得するために、再帰的な結合(Recursive Join)か、アプリケーション層での再帰処理が必要になる。MySQL 8.0以降であれば `WITH RECURSIVE`(共通テーブル式)が使えるが、WordPressの動作要件は依然として古い環境を許容しており、かつコアの `WP_Comment_Query` はこの恩恵を十分に受けられない。

そこで、Path Enumeration(パス列挙)だ。
各コメントに `/00001/00005/00012/` のような「根から自身に至るまでのIDの系譜」を文字列として持たせる。

この設計の利点:

  • 単一クエリで全子孫を取得可能: `WHERE comment_path LIKE ‘/00001/%’`
  • ソートが極めて容易: 文字列ソートがそのまま階層順のソートになる。
  • インデックスの恩恵: 前方一致検索(`LIKE ‘prefix%’`)はB-Treeインデックスを効率的に利用できる。

—

2. 実装:データベース・スキーマの拡張

まずは `wp_comments` にパスを保存するカラムを追加する。メタテーブル(`wp_commentmeta`)に逃げるな。メタテーブルはJOINのコストが高く、インデックス効率も悪い。高速化が目的ならば、迷わずテーブルを `ALTER` する。

— コメントパスを保持するカラムを追加
— 桁数を揃える(Zero-padding)ことで、文字列ソートを階層順と一致させる
ALTER TABLE wp_comments ADD COLUMN comment_path VARCHAR(255) NOT NULL DEFAULT ”;
CREATE INDEX idx_comment_path ON wp_comments(comment_path);

—

3. ロジックの注入:コメント投稿時のパス自動生成

コメントが挿入された瞬間、親の `comment_path` を引き継ぎ、自身のIDを付与して保存する。WordPressの `comment_post` フックを利用する。

/

  • コメント投稿時にPath Enumeration用のパスを生成・保存する
  • @param int $comment_id
  • @param int|string $comment_approved
  • @param array $commentdata

/
add_action(‘comment_post’, function($comment_id, $comment_approved, $commentdata) {
global $wpdb;

$parent_id = (int) $commentdata[‘comment_parent’];
$current_path = ”;

// IDを5桁でゼロ埋め(最大99,999件以上の階層深さを考慮するなら桁数を調整)
$padded_id = sprintf(‘%05d’, $comment_id);

if ($parent_id > 0) {
// 親のパスを取得
$parent_path = $wpdb->get_var($wpdb->prepare(
“SELECT comment_path FROM {$wpdb->comments} WHERE comment_ID = %d”,
$parent_id
));
$current_path = $parent_path . $padded_id . ‘/’;
} else {
// トップレベルコメント
$current_path = ‘/’ . $padded_id . ‘/’;
}

// 自身のパスを更新
$wpdb->update(
$wpdb->comments,
[‘comment_path’ => $current_path],
[‘comment_ID’ => $comment_id]
);
}, 10, 3);

—

4. 取得:再帰なしの単一クエリ・レンダリング

さて、ここからが本番だ。特定の記事(`post_id`)に紐づくコメント全件を、階層構造を保ったまま一気に取得する。

/

  • パス列挙モデルを利用して、階層構造を保ったコメントリストを高速取得する
  • @param int $post_id
  • @return array

/
function get_hierarchical_comments_fast(int $post_id) {
global $wpdb;

// comment_pathでソートするだけで、
// 「親 -> その子1 -> その孫 -> その子2」という理想的な順序で返ってくる。
$results = $wpdb->get_results($wpdb->prepare(”
SELECT FROM {$wpdb->comments}
WHERE comment_post_ID = %d
AND comment_approved = ‘1’
ORDER BY comment_path ASC
“, $post_id));

return $results;
}

// 使用例
$comments = get_hierarchical_comments_fast(123);

foreach ($comments as $comment) {
// パスの深さをスラッシュの数から計算(インデント等に使用)
$depth = substr_count(trim($comment->comment_path, ‘/’), ‘/’) + 1;

echo str_repeat(‘ ‘, $depth) . “ID: {$comment->comment_ID} – {$comment->comment_author}\n”;
}

—

5. パフォーマンス上の注意点と「プロ」の視点

この設計を実戦で運用するなら、以下の3点は必ず押さえておけ。

A. ゼロ埋め(Zero-padding)の重要性

なぜ `sprintf(‘%05d’, $id)` を使うのか。それは、文字列としてのソート順をIDの数値順と一致させるためだ。これを怠ると、パス `/1/` の次に `/10/` が来てしまい、`/2/` がその後に回されるという辞書順の罠にハマる。

B. 移動(Move)への対応

WordPressの標準仕様ではコメントの親を後から変更するUIはほぼ存在しないが、もしシステム的に「コメントの移動」を許容する場合、そのコメント以下の全子孫の `comment_path` を `REPLACE` 関数等で一括置換する処理が必要になる。

C. キャッシュ戦略

この手法はDBクエリを劇的に減らすが、さらに上を目指すなら、取得した結果セットを `wp_cache_set` でオブジェクトキャッシュ(Redis/Memcached)に叩き込め。パス列挙モデルは「一度構築してしまえば、読み取りが圧倒的に速い」のが最大の特徴だ。

結論

`comment_parent` を追いかける再帰コードは、エンジニアとしての怠慢だ。
データ量が増えることが予見されるシステムにおいて、物理構造に「パス」という概念を持ち込むことは、スケーラビリティを確保するための定石である。

WordPressを単なるCMSとしてではなく、堅牢なアプリケーションプラットフォームとして扱うのであれば、このようにRDBの特性を最大限に活かした設計を常に意識せよ。

コードは美しく、クエリは鋭く。それが「WordPressを掌握する」ということだ。

タイトルとURLをコピーしました