はじめに
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 がまだある |
WYSIWYG | Gutenberg Block Editor(React 製) | 伝統的リッチテキスト+プレビュー分割が主流 |
API 形態 | REST / GraphQL* / XML-RPC (*WPGraphQL プラグイン準公式化) | REST 片側のみ/独自 SDK 強制など |
マルチサイト | 1 インストール → 複数サイト/サブドメイン展開可 | SaaS 版にだけ機能を切り分け…がありがち |
✔︎ 規模別ユースケース
- スモールビジネス:テーマ+数本のプラグインで即公開。
- メディア企業:WordPress VIP や Pressable が CDN+DevOps を丸抱え。
- ヘッドレス:Next.js や Remix と
@wordpress/data
でフロントを自由設計。 - マルチテナント SaaS:
wp-config
でDOMAIN_CURRENT_SITE
を切り替え、1 コアで 1000 ドメイン運用実績(Linchpin社長インタビュー)。
✔︎ 本格 CMSと呼べる理由
- コンテンツモデルを「開発者が後付けで組める」
— CPT+ACF+カスタムタクソノミーで Doctrine 並みに設計可。 - 編集者 UI が常に追随
— 設計を変えても Gutenberg 側がリアルタイム生成。 - 10 年以上の後方互換を保ったまま大型進化
— Block Editor、Site Editor、Interactivity API…「革命を互換付きで」やってのけた稀有なケース。

プラグイン=黒魔術の温床って思われがちだけど、コアが壊れにくいからこそ魔改造できる側面もあるんだよ〜
主要 OSS CMS の “いま” を把握する
まずは 2025-05-13 時点の GitHub Stars で、周辺 CMS の勢力図を俯瞰しよう。WordPress 本体は SVN 開発だが、GitHub には read-only ミラー (WordPress/WordPress
) が存在するので、そこも含めて比較する。
順位 | CMS | Stars* | 備考 |
---|---|---|---|
① | Strapi | 66.6 k GitHub | Headless/Node.js |
② | Ghost | 49.2 k GitHub | モダンブログ SaaS 併設 |
③ | Directus | 30.5 k GitHub | SQL フル活用型 |
④ | WordPress (GitHub mirror) | 20.1 k GitHub | 本家は SVN |
⑤ | Decap CMS (旧 Netlify CMS) | 18.3 k GitHub | Git ベース |
⑥ | Keystone JS | 9.5 k GitHub | GraphQL/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_postmeta
は wp_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すると以下のようになる:
ID | post_title | meta_key | meta_value |
---|---|---|---|
3910 | Hello world | _thumbnail_id | 3915 |
3910 | Hello world | _wp_attached_file | hello.jpg |
3910 | Hello world | subtitle | こんにちは |
3910 | Hello world | _edit_lock | 1715612345:1 |
3910 | Hello world | _yoast_wpseo_title | Hello 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';
ID | post_title | thumbnail_id | subtitle |
---|---|---|---|
3910 | Hello world | 3915 | こんにちは |
3911 | Sample Post | (NULL) | (NULL) |
3912 | 夏フェスまとめ | 3920 | サマソニ2025 |
3913 | Plugin Review | (NULL) | ベータ版 |
3914 | Release Notes | 3919 | v2.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地獄を表した図
コメントを残す