メシのタネ

メシのタネになる、Laravelや設計思想の技術配信サイト


WordPressのデータベース構造を読み解く:混沌を制する柔構造のデザイン


  1. WordPress
  2. WordPressのデータベース構造を読み解く:混沌を制する柔構造のデザイン

はじめに

WordPressは世界で最も使われているCMSのひとつだが、その内部構造、特にデータベース設計について深掘りされる機会は意外と少ない。

「全部 wp_posts に突っ込むらしい」とか「meta テーブルがカオス」といった雑な理解で語られがち。

今回の記事では、WordPress があえて“ゆるい正規化”を選び続けた理由を、実務目線+愛情 120 % で紐解く試み。

本格CMSとしての WordPress

「WordPress=ブログ用 PHP スクリプト」——その刷り込み、まだ残ってない?
実は2005 年頃に終わった話です。いま WP コアが抱える機能セットは、OSSのくせに超リッチ。

領域WordPress コアで担保他 CMS では…
アクセス制御権限ロール+ Capability API (Editor/Author/Contributor/Custom …)商用でもロール追加は有料エディションな事例多数
多言語対応locale 切替・翻訳 API を標準同梱プラグイン拡張必須/β 機能扱いが多い
メディア管理画像リサイズ/メタ保存/REST Media エンドポイント別途 DAM(Digital Asset Mgr)と連携が定番
拡張フックActions / Filters 50 0 0 + 箇所コア改変が前提、アップデート不可になる CMS がまだある
WYSIWYGGutenberg Block Editor(React 製)伝統的リッチテキスト+プレビュー分割が主流
API 形態REST / GraphQL* / XML-RPC (*WPGraphQL プラグイン準公式化)REST 片側のみ/独自 SDK 強制など
マルチサイト1 インストール → 複数サイト/サブドメイン展開可SaaS 版にだけ機能を切り分け…がありがち

✔︎ 規模別ユースケース

  • スモールビジネス:テーマ+数本のプラグインで即公開。
  • メディア企業WordPress VIPPressableCDN+DevOps を丸抱え。
  • ヘッドレス:Next.js や Remix と @wordpress/data でフロントを自由設計。
  • マルチテナント SaaSwp-configDOMAIN_CURRENT_SITE を切り替え、1 コアで 1000 ドメイン運用実績(Linchpin社長インタビュー)。

✔︎ 本格 CMSと呼べる理由

  1. コンテンツモデルを「開発者が後付けで組める」
    — CPT+ACF+カスタムタクソノミーで Doctrine 並みに設計可。
  2. 編集者 UI が常に追随
    — 設計を変えても Gutenberg 側がリアルタイム生成。
  3. 10 年以上の後方互換を保ったまま大型進化
    — Block Editor、Site Editor、Interactivity API…「革命を互換付きで」やってのけた稀有なケース。
たねまる

プラグイン=黒魔術の温床って思われがちだけど、コアが壊れにくいからこそ魔改造できる側面もあるんだよ〜

主要 OSS CMS の “いま” を把握する

まずは 2025-05-13 時点の GitHub Stars で、周辺 CMS の勢力図を俯瞰しよう。WordPress 本体は SVN 開発だが、GitHub には read-only ミラー (WordPress/WordPress) が存在するので、そこも含めて比較する。

順位CMSStars*備考
Strapi66.6 k GitHubHeadless/Node.js
Ghost49.2 k GitHubモダンブログ SaaS 併設
Directus30.5 k GitHubSQL フル活用型
WordPress (GitHub mirror)20.1 k GitHub本家は SVN
Decap CMS (旧 Netlify CMS)18.3 k GitHubGit ベース
Keystone JS9.5 k GitHubGraphQL/React
たねまる

WordPress くん、SVNが本家なのに GitHub で 2 万超えは反則だよね〜。実質 100 万スターってことでヨシ!?

wp_posts —— すべては 1 枚のテーブルから

WordPress の心臓部は wp_posts。名前に post”とあるが、ここに入るのは:

  • 通常投稿 (post)
  • 固定ページ (page)
  • メディア (attachment)
  • カスタム投稿タイプ(任意)
SELECT ID, post_type, post_status, post_title FROM wp_posts LIMIT 10;

この1枚のテーブルで、WordPressはコンテンツという抽象エンティティを扱えるようにしている。

画像は post_parent で “ゆるく” 投稿と紐づくため、実態は 1:1 だけど多対多っぽい再利用も可能。

たねまる

一緒のテーブルで暮らす家族感、好きだな〜

wp_postmeta: 無限拡張フィールド

wp_postmetawp_posts と 1:N で紐づいており、各投稿の属性(カスタムフィールド)を格納する。

  • アイキャッチ画像のID(_thumbnail_id
  • 画像のファイル名(_wp_attached_file
  • メタデータ(_wp_attachment_metadata
  • カスタム投稿フィールド(任意)
SELECT * FROM wp_postmeta WHERE post_id = 3910;

この仕組みのすごさは、新しいフィールドをDB設計変更せずに追加できること。 まさに「CMSらしい柔軟性」を担っている。

見ての通り、投稿は1件なのに、metaの分だけ行が無限に膨らむ。

SELECT p.ID, 
p.post_title, 
m.meta_key, 
m.meta_value
FROM wp_posts p
JOIN wp_postmeta m ON p.ID = m.post_id
WHERE p.ID = 3910;

このようにJOINすると以下のようになる:

IDpost_titlemeta_keymeta_value
3910Hello world_thumbnail_id3915
3910Hello world_wp_attached_filehello.jpg
3910Hello worldsubtitleこんにちは
3910Hello world_edit_lock1715612345:1
3910Hello world_yoast_wpseo_titleHello world

1レコードがメタ数だけ縦に爆増(380くらいの投稿に対して6500くらい)。 → 表にしたければ meta_keyごとに個別 JOIN するしかない。

-- meta_keyを個別にJOINして横並びの形式にする例
SELECT p.ID, p.post_title,
       m1.meta_value AS thumbnail_id,
       m2.meta_value AS subtitle
FROM wp_posts p
LEFT JOIN wp_postmeta m1 ON p.ID = m1.post_id AND m1.meta_key = '_thumbnail_id'
LEFT JOIN wp_postmeta m2 ON p.ID = m2.post_id AND m2.meta_key = 'subtitle'
WHERE p.post_type = 'post';
IDpost_titlethumbnail_idsubtitle
3910Hello world3915こんにちは
3911Sample Post(NULL)(NULL)
3912夏フェスまとめ3920サマソニ2025
3913Plugin Review(NULL)ベータ版
3914Release Notes3919v2.1.0

この形式なら1投稿=1行で横に展開されるので、表として使える。 ただし、JOINするkeyをすべて知っておく必要があるというのは変わらない。

たねまる

意味のあるJOINをするには知識と祈りが要るんだね〜

wp_terms + wp_term_taxonomy + wp_term_relationships:分類の三位一体

WordPressのカテゴリ・タグ・カスタム分類は、3テーブルで構成される:

  • wp_terms:分類の名前などの共通部分
  • wp_term_taxonomy:タグ/カテゴリなど区別
  • wp_term_relationships:投稿と分類をつなぐ
-- 投稿ID 3909 のカテゴリ取得
SELECT t.name, tt.taxonomy
FROM wp_term_relationships tr
JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
JOIN wp_terms t ON tt.term_id = t.term_id
WHERE tr.object_id = 3909;

新しいカスタムタクソノミーを噛ませるときは

register_taxonomy()register_post_type( taxonomies => [] )二重宣言 が必須。

順序ミスると管理画面に出ず、静かに爆死する静かな罠。

WordPressは「投稿タイプとタクソノミーが 登録時に互いの存在を名簿に書き込む方式」なので、両者が同時に名乗り合わないと関係そのものが成立しない。

たねまる

taxonomy使いたいって言ったら2ヶ所に書く必要の謎、解けたよね〜

wp_options:WordPressの脳

サイト設定、プラグインの状態、テーマのカスタマイズ……あらゆる「サイトのグローバルな状態」が保存されている。

  • siteurl
  • home
  • active_plugins
  • rewrite_rules
  • theme_mods_{テーマ名}
SELECT option_name, option_value FROM wp_options WHERE autoload = 'yes';

特に autoload = yes のレコードは、ページロード時に自動的に読み込まれる。 これは設定の即時アクセスを可能にする工夫でもある。

たねまる

脳を全部メモリに載せるの、そろそろ卒業したいよね〜

まとめ —— ほとんど正規化せずに 20 年走り続けた理由

  • テーブル数を抑えて導入ハードルを下げる
  • カスタムフィールドで表現力を補完
  • REST APIで柔軟に外部連携

WordPress は 「RDB のお作法」より「実務で壊れにくい運用」 を最優先し、Gutenberg など巨大アップデートを飲み込んできた。言うなれば技術なんかよりも皆んなの「できたらいいな✨」の実現に尽力してきた結果と言えると思う。

おまけ:WordPressの柔構造

flowchart TB

  %% ① 元テーブル
  subgraph original["① 元テーブル"]
    direction LR
    P["wp_posts<br/>ID:3910<br/>post_title:Hello world"]
    M1["wp_postmeta<br/>ID:1<br/>meta_key:_thumbnail_id<br/>meta_value:3915"]
    M2["wp_postmeta<br/>ID:2<br/>meta_key:_wp_attached_file<br/>meta_value:hello.jpg"]
    M3["wp_postmeta<br/>ID:3<br/>meta_key:subtitle<br/>meta_value:こんにちは"]

    P -->|1 : N| M1
    P -->|1 : N| M2
    P -->|1 : N| M3
  end

殺伐としてJOIN地獄を表した図


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.