数据库与查询实战
这一课你将能做到
- 画出 WP 核心表的关系图,说清 posts / postmeta / options / users / terms 五大块各存什么
- 用 $wpdb 写出带 prepare 的安全查询,并用 dbDelta 规范建自定义表
- 用 WP_Query 的 meta_query / tax_query 组合出复杂课程列表,并知道性能坑在哪
为什么要学
WP 把你的所有内容——文章、页面、课程、设置——全部装进十来张 MySQL 表。做数据相关的开发(自定义字段筛选、批量导入、性能调优),绕不开表结构和 $wpdb。这也是 WP 面试区分「会用」和「会做」的分水岭。
人话:wp_posts 像 Java 里的通用实体表,post_type 字段区分文章/页面/课程;wp_postmeta 是典型的 EAV(键值对)扩展表——灵活但天生难索引,meta_query 写狠了就是慢查询之源。
先建立图像
wp_posts (ID, post_title, post_content, post_type, post_status)
│ 1:N │ N:M
▼ ▼
wp_postmeta wp_term_relationships
(post_id, meta_key, (object_id, term_taxonomy_id)
meta_value) │
▼
wp_options (option_name, wp_term_taxonomy → wp_terms
option_value, autoload) (分类法:category/tag/自定义)
wp_users (ID, user_login) ─1:N→ wp_usermeta
记忆法:四棵树——内容树(posts+meta)、配置树(options)、用户树(users+meta)、词汇树(terms 家族)。
跟着做
步骤 1:先看看库里有什么
wp db tables # 列出全部表
wp db query "SELECT post_type, COUNT(*) AS n FROM wp_posts GROUP BY post_type"
wp db size --tables # 哪张表最大
打开 phpMyAdmin 逐行看 wp_posts:一篇「课程」就是一行 post_type='course' 的记录,post_content 里存的可能是带区块注释的文本。
步骤 2:$wpdb 三剑客
global $wpdb;
// get_var: 取单个值
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
'course', 'publish'
)
);
// get_row: 取一行(对象)
$course = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", 42 )
);
echo esc_html( $course->post_title );
// get_results: 取多行
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT p.ID, p.post_title
FROM {$wpdb->posts} p
WHERE p.post_type = %s AND p.post_status = %s
ORDER BY p.post_date DESC
LIMIT %d",
'course', 'publish', 10
)
);
注意 {$wpdb->posts}:用属性而非硬编码 wp_posts,尊重自定义表前缀。%s/%d 占位符是 prepare 的防注入核心。
步骤 3:建一张自定义表
数据量大、查询复杂的场景(如课程学习记录)建独立表,在插件激活钩子里:
register_activation_hook( __FILE__, function () {
global $wpdb;
$table = $wpdb->prefix . 'qrocmq_enrollments';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) UNSIGNED NOT NULL,
course_id BIGINT(20) UNSIGNED NOT NULL,
progress TINYINT(3) UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY user_course (user_id, course_id),
KEY course_idx (course_id)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
} );
dbDelta 很挑剔:每字段独立一行、PRIMARY KEY 后两个空格、KEY 而非 INDEX——照抄官方示例格式最稳。
步骤 4:WP_Query 组合查询
需求:发布中的「course」,难度=进阶、时长 10-50 小时、属于 Java 分类:
$query = new WP_Query( array(
'post_type' => 'course',
'post_status' => 'publish',
'posts_per_page' => 12,
'paged' => max( 1, get_query_var( 'paged' ) ),
'tax_query' => array(
array(
'taxonomy' => 'course_cat',
'field' => 'slug',
'terms' => 'java',
),
),
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'level',
'value' => 'advanced',
),
array(
'key' => 'duration',
'value' => array( 10, 50 ),
'type' => 'NUMERIC',
'compare' => 'BETWEEN',
),
),
) );
数字比较必须 type => NUMERIC,否则按字符串比较,BETWEEN 结果全错——经典暗坑。
步骤 5:看清 wp_options 与 autoload
wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options
WHERE autoload='yes' ORDER BY size DESC LIMIT 10"
autoload=yes 的选项每次请求全量载入内存。transients(_transient_*)也住这里——塞大数组进 autoload 选项是隐形性能杀手。
讲清楚要点
- meta 表是 EAV 模型:一行一个键值对,查「有某字段的文章」需要 JOIN,
meta_value多为 LONGTEXT,LIKE '%x%' 无法走索引——复杂筛选要么控制规模,要么上自定义表。 - $wpdb->prepare 是铁律:占位符 %s/%d,永远不拼接变量;表名不能参数化,用 {$wpdb->posts} 属性。
- WP_Query 生成的 SQL 是安全的:内部已转义,优先用 WP_Query/Tax_Query 而不是手写 SQL——可读、可缓存(对象缓存自动覆盖)、可被插件过滤。
- 读写分离的关注点:wp_options 管「站点级配置」(少而小),postmeta 管「内容级扩展」(随内容量增长),高频聚合统计走自定义表+索引。
- 缓存意识:同一请求重复查询相同数据,先查 wp_cache_get / 存 transient,第 11 课性能课展开。
你可能会懵的地方
-
post_content 里那些 是什么? 区块注释:每段内容的类型与属性序列化。这就是「文章也是数据」的直观体现,REST API 返回的 rendered HTML 是渲染后的结果。
-
wp_posts 里大量 post_type='revision'? 每次保存生成的历史版本。通过 WP_POST_REVISIONS 常量限制(
define('WP_POST_REVISIONS', 5)),数据清理用wp post delete $(wp post list --post_type=revision --field=ID)。 -
meta_query 为什么慢? 每个条件都要 JOIN 一次 postmeta 表,条件越多 JOIN 越多;且 meta_value 是文本列,NUMERIC 比较也要函数转换。十条以内的条件组合没问题,上千行数据 + 多层嵌套就要换方案。
-
$wpdb->prefix 有什么用? 支持多站点/一库多站:每站表前缀不同(wp_、wp2_)。硬编码 wp_ 的代码在改前缀的站点上直接崩。
-
自定义字段该用 postmeta 还是自定义表? 低频读写、跟内容同生命周期 → postmeta;高频统计、需要多列索引、行数百万级 → 自定义表。判断标准是「查询复杂度」。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| "WHERE post_title = '$var'" | SQL 注入 | $wpdb->prepare + 占位符 |
| 硬编码表名 wp_posts | 改前缀站点全线崩溃 | {$wpdb->posts} 与 $wpdb->prefix |
| meta_query 数字比较不加 type | 字符串序比较,结果错乱 | type => 'NUMERIC' |
| autoload 选项存大数组 | 每次请求全站内存膨胀 | 拆小或改存 transient/对象缓存 |
| 用 post_title 做精确匹配查询 | 无索引、慢 | 查 ID 或给标题加索引的自定义表 |
卡点急救
- get_results 返回空:先在 phpMyAdmin 跑同款 SQL;核对 post_status(默认查不到草稿)、表前缀。
- prepare 报错参数数量不符:占位符个数与传入参数必须一一对应。
- dbDelta 没建出表:格式不合规(多字段一行、小写 KEY、缺两个空格);对照官方示例逐字符检查。
面试常问
-
取 10 篇文章及其 3 个自定义字段,WP 层与 SQL 层各发生什么? WP_Query 先查 wp_posts(LIMIT 10),meta 通过 update_meta_cache 批量一次 IN 查询取回(并非每篇一查);等价 SQL 是 posts 一次 + postmeta 一次 JOIN/IN,理解这点才能解释为什么 meta 查询要控制数量。
-
如何优化一个 meta_query 很重的列表页? 手段:限制 posts_per_page、给 meta 查询加 type 校验、热门条件冗余进 taxonomy 或自定义表建索引、结果用 transient/对象缓存、数据量大后迁自定义表。
-
$wpdb->prepare 的作用与局限? 占位符参数化防 SQL 注入;但只保护值,表名/列名/排序方向不能参数化,需白名单校验。
自测三问
- 画出 wp_posts 与 postmeta、terms 两类关系,并说明各自的外键字段。
- 为什么 meta_value 上做 LIKE '%java%' 慢?两种工程上的解法?
- dbDelta 对 SQL 格式有哪些挑剔要求?举三条。
小结一句话
WP 的数据库是「内容树 + 配置树 + 用户树 + 词汇树」四棵树,日常优先 WP_Query,手写必走 prepare,复杂的筛选与统计在数据量增长后诚实地迁往带索引的自定义表。
延伸
- $wpdb 类参考
- WP_Meta_Query 参考
- 下一课:REST API 与 Headless——把 WP 接进本站同款 Next.js 技术栈。