前言

如果你有自己架 WordPress 部落格,可能每天都在用它的後台發文章、裝外掛、改設定,但你有沒有好奇過:這些資料在資料庫裡到底長什麼樣子?

我一直覺得,理解一個系統的資料庫結構,是真正掌握這個系統的起點。WordPress 的資料庫設計其實非常巧妙——只用 12 張表就支撐了一個完整的 CMS 系統,而且它的 meta 系統設計模式在很多其他專案裡也值得借鏡。

這篇文章我會帶你逐一拆解 WordPress 的核心表結構,分析它的設計哲學,最後分享一些實用的查詢最佳化技巧。


WordPress 資料庫全貌

WordPress 預設使用 MySQL/MariaDB,標準安裝有 12 張表(表名前綴預設是 wp_):

-- 查看所有 WordPress 表
SHOW TABLES LIKE 'wp_%';

| 表名 | 用途 | 資料量級 |
|——|——|———|
| wp_posts | 文章、頁面、附件等所有內容 | 核心 |
| wp_postmeta | 文章的擴充屬性 | 可能非常大 |
| wp_comments | 留言 | 中等 |
| wp_commentmeta | 留言的擴充屬性 | 小 |
| wp_users | 使用者 | 小 |
| wp_usermeta | 使用者的擴充屬性 | 中等 |
| wp_terms | 分類法的詞彙(分類、標籤) | 小 |
| wp_term_taxonomy | 分類法定義 | 小 |
| wp_term_relationships | 內容與分類的關聯 | 中等 |
| wp_options | 全站設定 | 需注意 autoload |
| wp_links | 連結(已棄用) | 忽略 |
| wp_termmeta | 分類法的擴充屬性 | 小 |


核心表詳解

wp_posts — 一張表裝所有內容

這是 WordPress 最重要的表,所有「內容」都存在這裡:

-- wp_posts 結構(簡化版)
CREATE TABLE wp_posts (
    ID                  BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    post_author         BIGINT UNSIGNED DEFAULT 0,        -- 作者 ID
    post_date           DATETIME DEFAULT CURRENT_TIMESTAMP, -- 發布時間
    post_date_gmt       DATETIME,                          -- GMT 時間
    post_content        LONGTEXT NOT NULL,                 -- 內容(HTML)
    post_title          TEXT NOT NULL,                      -- 標題
    post_excerpt        TEXT NOT NULL,                      -- 摘要
    post_status         VARCHAR(20) DEFAULT 'publish',     -- 狀態
    comment_status      VARCHAR(20) DEFAULT 'open',        -- 留言開關
    ping_status         VARCHAR(20) DEFAULT 'open',
    post_password       VARCHAR(255) DEFAULT '',           -- 密碼保護
    post_name           VARCHAR(200) DEFAULT '',           -- URL slug
    post_modified       DATETIME,
    post_modified_gmt   DATETIME,
    post_parent         BIGINT UNSIGNED DEFAULT 0,         -- 父文章 ID
    guid                VARCHAR(255) DEFAULT '',
    menu_order          INT DEFAULT 0,
    post_type           VARCHAR(20) DEFAULT 'post',        -- 類型
    post_mime_type      VARCHAR(100) DEFAULT '',
    comment_count       BIGINT DEFAULT 0,

KEY post_name (post_name(191)), KEY type_status_date (post_type, post_status, post_date, ID), KEY post_parent (post_parent), KEY post_author (post_author) );

post_type 是關鍵欄位,WordPress 用它來區分不同類型的內容:

-- 查看所有文章類型
SELECT DISTINCT post_type, COUNT(*) AS count
FROM wp_posts
GROUP BY post_type;

-- 常見的 post_type: -- 'post' → 一般文章 -- 'page' → 靜態頁面 -- 'attachment' → 上傳的圖片/檔案 -- 'revision' → 文章修訂版本 -- 'nav_menu_item' → 選單項目 -- 'custom_css' → 自訂 CSS -- 外掛也可以註冊自訂的 post_type

-- 查詢所有已發布的文章
SELECT ID, post_title, post_date, post_name
FROM wp_posts
WHERE post_type = 'post'
  AND post_status = 'publish'
ORDER BY post_date DESC
LIMIT 10;

-- 查詢某篇文章的所有修訂版本 SELECT ID, post_title, post_modified, post_content FROM wp_posts WHERE post_parent = 42 -- 原始文章 ID AND post_type = 'revision' ORDER BY post_modified DESC;

wp_postmeta — EAV 模式的擴充屬性

這是 WordPress 設計中最巧妙(也最具爭議)的部分。它採用 EAV(Entity-Attribute-Value) 模式,讓任何外掛都能在不改表結構的情況下,為文章新增自訂欄位。

CREATE TABLE wp_postmeta (
    meta_id     BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    post_id     BIGINT UNSIGNED DEFAULT 0,
    meta_key    VARCHAR(255) DEFAULT NULL,
    meta_value  LONGTEXT,

KEY post_id (post_id), KEY meta_key (meta_key(191)) );

-- 查看某篇文章的所有 meta
SELECT meta_key, meta_value
FROM wp_postmeta
WHERE post_id = 42;

-- 常見的 meta_key: -- '_thumbnail_id' → 特色圖片的附件 ID -- '_edit_lock' → 編輯鎖定 -- '_wp_page_template' → 頁面範本 -- 外掛自訂的 meta,例如 SEO 外掛的 '_yoast_wpseo_title'

-- 查詢有特色圖片的文章 SELECT p.ID, p.post_title, pm.meta_value AS thumbnail_id FROM wp_posts p JOIN wp_postmeta pm ON p.ID = pm.post_id WHERE p.post_type = 'post' AND p.post_status = 'publish' AND pm.meta_key = '_thumbnail_id';

-- 查詢自訂欄位(例如文章的難度等級) SELECT p.post_title, pm.meta_value AS difficulty FROM wp_posts p JOIN wp_postmeta pm ON p.ID = pm.post_id WHERE pm.meta_key = 'difficulty_level' AND pm.meta_value = 'advanced';

wp_options — 全站設定的鍵值儲存

CREATE TABLE wp_options (
    option_id    BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    option_name  VARCHAR(191) UNIQUE,
    option_value LONGTEXT NOT NULL,
    autoload     VARCHAR(20) DEFAULT 'yes'  -- 是否每次請求都載入
);
-- 查看重要的全站設定
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN (
    'siteurl', 'home', 'blogname', 'blogdescription',
    'admin_email', 'template', 'stylesheet',
    'active_plugins', 'current_theme'
);

-- 查看 autoload 的資料量(這很重要!) SELECT autoload, COUNT(*) AS count, ROUND(SUM(LENGTH(option_value)) / 1024 / 1024, 2) AS size_mb FROM wp_options GROUP BY autoload;

-- 找出最大的 autoload 選項(效能殺手) SELECT option_name, LENGTH(option_value) AS value_size, LEFT(option_value, 100) AS preview FROM wp_options WHERE autoload = 'yes' ORDER BY LENGTH(option_value) DESC LIMIT 20;

autoload 欄位非常重要:設為 yes 的選項會在每次頁面載入時被讀取到記憶體中。如果有外掛把大量資料存成 autoload,會嚴重影響效能。

分類法系統(Terms)

WordPress 的分類法系統由三張表組成:

-- wp_terms:存放詞彙(分類名稱、標籤名稱)
CREATE TABLE wp_terms (
    term_id    BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name       VARCHAR(200) DEFAULT '',    -- 顯示名稱
    slug       VARCHAR(200) DEFAULT '',    -- URL 用的 slug
    term_group BIGINT DEFAULT 0
);

-- wp_term_taxonomy:定義分類法類型 CREATE TABLE wp_term_taxonomy ( term_taxonomy_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, term_id BIGINT UNSIGNED DEFAULT 0, taxonomy VARCHAR(32) DEFAULT '', -- 'category', 'post_tag' 等 description LONGTEXT NOT NULL, parent BIGINT UNSIGNED DEFAULT 0, -- 父分類 count BIGINT DEFAULT 0 -- 文章數量快取 );

-- wp_term_relationships:多對多關聯表 CREATE TABLE wp_term_relationships ( object_id BIGINT UNSIGNED DEFAULT 0, -- post ID term_taxonomy_id BIGINT UNSIGNED DEFAULT 0, -- 分類法 ID term_order INT DEFAULT 0, PRIMARY KEY (object_id, term_taxonomy_id) );

-- 查詢所有分類及其文章數
SELECT t.name, tt.taxonomy, tt.count
FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
WHERE tt.taxonomy = 'category'
ORDER BY tt.count DESC;

-- 查詢某分類下的所有文章 SELECT p.ID, p.post_title, p.post_date FROM wp_posts p JOIN wp_term_relationships tr ON p.ID = tr.object_id 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 t.slug = 'databases' AND tt.taxonomy = 'category' AND p.post_status = 'publish' ORDER BY p.post_date DESC;

-- 查詢某篇文章的所有分類和標籤 SELECT t.name, tt.taxonomy FROM wp_terms t JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = 42;


WordPress 的設計哲學

EAV 模式的利與弊

優點

  • 極高的彈性——任何外掛都能新增欄位,不需要 ALTER TABLE
  • 向後相容——核心表結構幾乎從不改變
  • 簡單——只需要理解 key-value 的概念

缺點

  • 查詢效能較差——需要 JOIN wp_postmeta 表
  • 資料型別不明確——meta_value 是 LONGTEXT,沒有型別約束
  • 索引效率低——meta_value 很難建有效的索引
-- 這種查詢在 wp_postmeta 很大時會很慢
SELECT p.post_title
FROM wp_posts p
JOIN wp_postmeta pm1 ON p.ID = pm1.post_id AND pm1.meta_key = 'price'
JOIN wp_postmeta pm2 ON p.ID = pm2.post_id AND pm2.meta_key = 'color'
WHERE CAST(pm1.meta_value AS DECIMAL) BETWEEN 100 AND 500
  AND pm2.meta_value = 'red';

如何在自己的專案中借鏡

如果你在設計自己的部落格系統,可以取 WordPress 的精華、避開它的問題:

-- 借鏡:保留 meta 的彈性,但限制在真正需要的場景
CREATE TABLE posts (
    id          SERIAL PRIMARY KEY,
    title       TEXT NOT NULL,
    slug        VARCHAR(200) UNIQUE NOT NULL,
    content     TEXT NOT NULL,
    excerpt     TEXT,
    status      VARCHAR(20) DEFAULT 'draft',
    author_id   INTEGER REFERENCES users(id),
    -- 把常用的欄位直接放在表裡(不用 meta)
    featured_image_url TEXT,
    reading_time_minutes INTEGER,
    -- 用 JSONB 取代 EAV(如果用 PostgreSQL)
    metadata    JSONB DEFAULT '{}',
    created_at  TIMESTAMPTZ DEFAULT NOW(),
    updated_at  TIMESTAMPTZ DEFAULT NOW()
);

-- JSONB 索引 CREATE INDEX idx_posts_metadata ON posts USING gin (metadata);

-- 查詢 JSONB 欄位 SELECT title FROM posts WHERE metadata->>'difficulty' = 'advanced' AND (metadata->>'views')::int > 1000;


查詢最佳化技巧

1. 清理 wp_postmeta

-- 找出孤兒 meta(對應的 post 已刪除)
SELECT COUNT(*) AS orphan_count
FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL;

-- 清理孤兒 meta DELETE pm FROM wp_postmeta pm LEFT JOIN wp_posts p ON pm.post_id = p.ID WHERE p.ID IS NULL;

-- 清理修訂版本(保留最近 5 個) DELETE FROM wp_posts WHERE post_type = 'revision' AND post_date < DATE_SUB(NOW(), INTERVAL 30 DAY);

2. 最佳化 wp_options

-- 把不需要每次載入的選項改為不自動載入
UPDATE wp_options
SET autoload = 'no'
WHERE option_name LIKE '%_transient_%'
  AND autoload = 'yes';

-- 清理過期的 transient(暫存資料) DELETE FROM wp_options WHERE option_name LIKE '%_transient_timeout_%' AND option_value < UNIX_TIMESTAMP();

DELETE a FROM wp_options a JOIN wp_options b ON b.option_name = CONCAT('_transient_timeout_', REPLACE(a.option_name, '_transient_', '')) WHERE a.option_name LIKE '%_transient_%' AND b.option_value < UNIX_TIMESTAMP();

3. 新增自訂索引

-- 如果你的外掛常用某個 meta_key 做查詢
CREATE INDEX idx_postmeta_custom ON wp_postmeta (meta_key(50), meta_value(50));

-- 如果你需要按 meta_value 排序 -- 注意:這只在 meta_value 長度有限時有效 CREATE INDEX idx_postmeta_price ON wp_postmeta (meta_key(20), post_id) WHERE meta_key = 'price'; -- MySQL 不支援 partial index,這是 PostgreSQL 語法

4. 監控慢查詢

-- 在 MySQL 設定中開啟慢查詢日誌
-- my.cnf:
-- slow_query_log = 1
-- long_query_time = 1
-- slow_query_log_file = /var/log/mysql/slow.log

-- 或用 EXPLAIN 分析具體查詢 EXPLAIN SELECT p.post_title FROM wp_posts p JOIN wp_postmeta pm ON p.ID = pm.post_id WHERE p.post_type = 'post' AND p.post_status = 'publish' AND pm.meta_key = 'featured' AND pm.meta_value = '1' ORDER BY p.post_date DESC LIMIT 10;


用 Python 操作 WordPress 資料庫

import mysql.connector
from datetime import datetime

conn = mysql.connector.connect( host="localhost", user="root", password="rootpass", database="wordpress" )

def get_recent_posts(limit=10): """取得最近的文章""" cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT p.ID, p.post_title, p.post_date, p.post_name AS slug, u.display_name AS author, GROUP_CONCAT(t.name) AS categories FROM wp_posts p LEFT JOIN wp_users u ON p.post_author = u.ID LEFT JOIN wp_term_relationships tr ON p.ID = tr.object_id LEFT JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id AND tt.taxonomy = 'category' LEFT JOIN wp_terms t ON tt.term_id = t.term_id WHERE p.post_type = 'post' AND p.post_status = 'publish' GROUP BY p.ID ORDER BY p.post_date DESC LIMIT %s """, (limit,)) return cursor.fetchall()

def get_site_stats(): """取得網站統計""" cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT (SELECT COUNT(*) FROM wp_posts WHERE post_type='post' AND post_status='publish') AS post_count, (SELECT COUNT(*) FROM wp_comments WHERE comment_approved='1') AS comment_count, (SELECT COUNT(*) FROM wp_users) AS user_count, (SELECT COUNT(*) FROM wp_terms t JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'category') AS category_count """) return cursor.fetchone()

# 使用 posts = get_recent_posts(5) for post in posts: print(f"[{post['post_date']}] {post['post_title']} by {post['author']}") print(f" Categories: {post['categories']}")

stats = get_site_stats() print(f"\nSite Stats: {stats['post_count']} posts, " f"{stats['comment_count']} comments, {stats['user_count']} users")


小結

WordPress 的資料庫設計是一個在「彈性」和「效能」之間取得平衡的經典案例。它的 EAV 模式讓整個外掛生態系得以蓬勃發展,但代價是在資料量大時需要額外的最佳化工作。

如果你正在維護 WordPress 網站,建議定期清理 postmeta 和 options 表中的垃圾資料,並監控 autoload 的資料量。如果你在設計自己的系統,可以考慮用 JSONB(PostgreSQL)來取代傳統的 EAV 模式,保留彈性的同時獲得更好的查詢效能。

延伸閱讀