2026年9月13日

2026年9月13日

WordPressのwp_termsとwp_term_taxonomyの関係と整合性チェック・修復の手順

WordPress のカテゴリーやタグは、wp_terms・wp_term_taxonomy・wp_term_relationships の3テーブルに分けて正規化されて保存されています。データベースに外部キー制約が無いため、直接の SQL 操作や移行の失敗で「親の無い行」や「件数のずれ」が生じることがあります。この記事では、テーブルの関係を整理したうえで、不整合を見つける SQL と、できるだけ WordPress の API を使って直す手順を紹介します。

3つのテーブルの役割と関係

まず、どのテーブルが何を持っているかを押さえます。以下ではテーブル接頭辞を wp_ として書きますが、実際の接頭辞は wp-config.php の $table_prefix に合わせてください。

  • wp_terms: 用語そのもの。term_id、名前の name、スラッグの slug、term_group を持ちます。
  • wp_term_taxonomy: 用語が「どのタクソノミーに属するか」。term_taxonomy_id、term_id、taxonomy(category、post_tag、独自タクソノミー名など)、description、親の parent、使用数の count を持ちます。
  • wp_term_relationships: 投稿などのオブジェクトと用語の結び付き。object_id と term_taxonomy_id の組で保存されます。
  • wp_termmeta: 用語ごとのメタ情報。term_id で wp_terms に結び付きます。

ここで間違えやすいのが ID の種類です。wp_term_relationships が参照するのは term_id ではなく term_taxonomy_id です。一方、wp_term_taxonomy の parent には親の term_id が入ります。この2つを取り違えた SQL を書くと、正常なデータを不整合と誤判定したり、逆に壊したりします。

なぜ2テーブルに分かれているのか

もともと WordPress は、同じ名前・スラッグの用語を複数のタクソノミーで共有できる設計でした。たとえば「ニュース」というカテゴリーとタグが、wp_terms の1行を共有できたわけです。この「共有ターム」は編集時に思わぬ影響を生むため、WordPress 4.2 以降、共有タームは更新時などに分割される方針に変わりました。分割が起きると split_shared_term アクションが発火し、wp_get_split_term() で旧 ID から新 ID を引けるようになっています。

古いサイトでは、テーマやプラグインが用語の term_id をオプションなどに保存している場合、分割後に古い ID を指したままになることがあります。整合性を調べるときは、この経緯も頭に入れておくと原因を推測しやすくなります。

データベースは整合性を守ってくれない

WordPress のコアテーブルには外部キー制約がありません。wp_term_taxonomy には term_id と taxonomy の組に一意制約がありますが、「参照先が存在するか」は PHP 側(wp_insert_term() や wp_delete_term() など)が守っています。つまり、API を通さずに SQL で行を削除したり、インポートが途中で止まったりすると、不整合がそのまま残ります。

コアテーブルに外部キーを後から追加することは勧めません。コアの更新処理やプラグインの想定と衝突するおそれがあるためです。整合性は定期的なチェックと API 経由の修正で保つのが現実的です。

不整合を見つける SQL

以下はすべて読み取り専用の SELECT です。本番で実行してもデータは変わりませんが、大きなサイトでは負荷がかかるため、アクセスの少ない時間帯かステージングのコピーで実行してください。

1. 用語本体の無いタクソノミー行

SELECT tt.term_taxonomy_id, tt.term_id, tt.taxonomy
FROM wp_term_taxonomy tt
LEFT JOIN wp_terms t ON t.term_id = tt.term_id
WHERE t.term_id IS NULL;

wp_terms 側だけが削除された状態です。管理画面には名前の無い用語として現れたり、表示されなかったりします。

2. どのタクソノミーにも属さない用語

SELECT t.term_id, t.name, t.slug
FROM wp_terms t
LEFT JOIN wp_term_taxonomy tt ON tt.term_id = t.term_id
WHERE tt.term_taxonomy_id IS NULL;

画面からは見えない「宙に浮いた」用語です。表示上の害は小さいものの、同じスラッグで新しい用語を作るときに紛らわしくなります。

3. 存在しない用語を指す関連付け

SELECT tr.object_id, tr.term_taxonomy_id
FROM wp_term_relationships tr
LEFT JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_taxonomy_id IS NULL;

4. 存在しない投稿を指す関連付け

SELECT tr.object_id, tt.taxonomy
FROM wp_term_relationships tr
JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
LEFT JOIN wp_posts p ON p.ID = tr.object_id
WHERE p.ID IS NULL
  AND tt.taxonomy IN ('category', 'post_tag');

object_id は投稿 ID とは限りません。リンク用の link_category は wp_links を、プラグインによってはユーザーなど別のオブジェクトを指すタクソノミーもあります。そのため、投稿に紐づくと分かっているタクソノミーだけに絞って調べます。

5. 親が存在しない階層

SELECT c.term_id, c.taxonomy, c.parent
FROM wp_term_taxonomy c
LEFT JOIN wp_term_taxonomy p
  ON p.term_id = c.parent AND p.taxonomy = c.taxonomy
WHERE c.parent <> 0
  AND p.term_taxonomy_id IS NULL;

親カテゴリーを SQL で削除したときなどに残ります。カテゴリーの一覧や階層表示が崩れる原因になります。

6. 共有タームと同一スラッグの重複

-- 複数のタクソノミーで共有されている term_id
SELECT term_id, COUNT(*) AS n
FROM wp_term_taxonomy
GROUP BY term_id
HAVING n > 1;

-- 同じタクソノミー内で同じスラッグが重複している
SELECT tt.taxonomy, t.slug, COUNT(*) AS n
FROM wp_terms t
JOIN wp_term_taxonomy tt ON tt.term_id = t.term_id
GROUP BY tt.taxonomy, t.slug
HAVING n > 1;

共有タームは古いサイトでは残っていても動作上の問題が無いことがあります。一方、同じタクソノミー内のスラッグ重複は、通常の API 経由では起きないため、インポートや直接操作の痕跡と考えられます。アーカイブ URL がどちらの用語を指すかが不定になるので、優先して整理します。

修復の進め方

作業前にバックアップを取る

修復は DB の書き換えです。対象テーブルだけでなく DB 全体のバックアップを取り、まずステージングで手順を通してから本番に適用します。WP-CLI が使える環境なら次のように取得できます。

wp db export before-term-fix.sql

件数のずれは recount で直す

wp_term_taxonomy の count は、投稿の公開・削除のたびに更新されるキャッシュのような値です。SQL で関連付けを消した後などはずれたままになります。単純な COUNT(*) と比べてはいけない点に注意してください。投稿用のタクソノミーでは、原則として公開済みの投稿だけが数えられるため、下書きを含む件数とは一致しません。

ずれを直すには WP-CLI の wp term recount を使います。WordPress 自身の数え方で再計算されます。

wp term recount category post_tag

独自タクソノミーがある場合は、その名前も並べて指定します。

用語の削除や付け替えは API を通す

不要な用語を消すときは、SQL の DELETE ではなく wp_delete_term() や WP-CLI の wp term delete を使います。API を通すと、関連付け・メタ・キャッシュの後始末と、関連するフックの実行が行われます。

# 例: category の term_id 123 を削除(投稿からの関連付けも外れる)
wp term delete category 123

既定の投稿カテゴリー(設定の「投稿用カテゴリーの初期設定」、オプション名 default_category)は削除できない仕様です。重複スラッグを整理するときは、残す側の用語に投稿を付け替えてから、もう一方を削除します。付け替えは wp_set_post_terms() や wp_set_object_terms() を使うと安全です。

API では拾えない「孤児」だけを SQL で消す

チェック 1・3 で見つかった行は、参照先がすでに無いため API から扱えないことがあります。この場合に限り、バックアップを取ったうえで SQL で削除します。対象を先に SELECT で確かめ、件数が想定と合うことを確認してから実行してください。

-- 存在しない term_taxonomy_id を指す関連付けを削除(チェック3の結果)
DELETE tr FROM wp_term_relationships tr
LEFT JOIN wp_term_taxonomy tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_taxonomy_id IS NULL;

親が存在しない階層(チェック 5)は、行を消すのではなく、管理画面や wp term update で親を付け直すか、parent を 0(最上位)に戻すのが無難です。

最後にキャッシュを消して表示を確認する

SQL で直接変更した内容は、オブジェクトキャッシュ(Redis など)に古い値が残っていると反映されません。修復後は wp cache flush を実行し、カテゴリー一覧、アーカイブページ、パンくずリスト、サイトマップを目で確認します。ページキャッシュを使っている場合は、その削除も忘れずに行います。

不整合を起こさないための運用

  • 用語の追加・削除・付け替えは wp_insert_term()、wp_update_term()、wp_delete_term()、wp_set_object_terms() など API を通す
  • 移行やインポートの後に、この記事のチェック SQL を一通り流す
  • 用語の ID を独自に保存しているコードでは、split_shared_term アクションを考慮する
  • 大量のデータ修正は、少量で試してから全体に広げる

これらの関数やフックの仕様は、WordPress のバージョンで細部が変わることがあります(2026年10月現在の情報です)。作業前に WordPress 公式の開発者リファレンスで、利用中のバージョンの説明を確認してください。

まとめ

  • wp_terms は用語本体、wp_term_taxonomy は所属と階層・件数、wp_term_relationships は投稿との結び付きを持ち、関連付けは term_taxonomy_id で行われる
  • DB に外部キーが無いため、整合性は PHP の API が守っている。SQL での直接操作が不整合の主な原因になる
  • 孤児行・存在しない親・同一スラッグの重複は、読み取り専用の SELECT で洗い出せる
  • 件数は wp term recount、削除や付け替えは API、参照先の無い孤児だけを SQL で消す
  • 作業はバックアップとステージングでの確認を先に行い、最後にキャッシュを消して表示を確かめる

お気軽にご相談ください

お見積りへ お問い合わせへ
▲