前言
如果你有自己架 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 模式,保留彈性的同時獲得更好的查詢效能。
延伸閱讀
- WordPress 資料庫描述(官方)
- WordPress Codex: Database Description
- WP-CLI — 用命令列管理 WordPress 的好工具
- Query Monitor 外掛 — 即時監控 WordPress 的資料庫查詢