在 WordPress 插件里建自定义表,通常不是在页面请求中随手执行一次 CREATE TABLE,而是把建表语句放进插件激活钩子,交给 dbDelta() 生成并比对表结构,再用一个 option 记录表结构版本,后续升级时只跑差异部分。是否真的需要新建表,要先看数据能不能用自定义文章类型、post meta 或 term meta 表达;只有当查询条件多、需要联合索引、数据量预期增长、或者语义上不属于文章和用户时,才值得单独建表。表名一律用 $wpdb->prefix 拼接,不要写死 wp_。
适用场景:插件需要存储结构固定、查询条件较多的业务数据。操作动作:在激活钩子里用
dbDelta()建表,字符集取$wpdb->get_charset_collate(),版本号写入 option;升级时先比版本再执行建表,避免每次请求都跑 DDL。验证方式:激活后查$wpdb->last_error、用 SHOW TABLES 和 DESCRIBE 核对字段与索引,停用再启用确认无重复报错。风险边界:dbDelta 对 SQL 书写格式敏感,默认值写法受 MySQL 严格模式影响,多站点网络激活不会自动覆盖所有子站点。
先确认当前场景和目标
动手写 SQL 之前,先确认几件事,它们直接决定后面代码怎么写:
- 数据归属:每条记录是否天然属于某篇文章或某个用户?如果属于,用
postmeta或usermeta通常够用,新建表反而增加维护成本。 - 查询形态:是否需要按多个字段组合筛选、排序、分页,或者需要给某个字段单独建索引。meta 表在这种场景下会随数据量变大而变慢,这是新建表的常见理由。
- 环境前提:数据库账号有没有 CREATE / ALTER 权限;站点字符集是否为 utf8mb4;是否为多站点网络;插件是单站点激活还是网络激活。
- 升级路径:是否已经存在旧版本的表或字段,需不需要迁移数据。这一步不确认,后面改结构时容易把已有数据写坏。
如果以上几条都指向“需要独立表”,就进入下一步;如果只是存一两个配置项,用 update_option() 更省事。
按这个顺序处理
顺序建议是:先定义表名和字段,再写符合 dbDelta 格式的建表语句,最后挂到激活钩子并加版本判断。下面是一段可替换的通用骨架,放在插件主文件里,把表名、字段和版本号换成自己的即可。
function demo_records_table() {
global $wpdb;
return $wpdb->prefix . 'demo_records';
}
function demo_records_install() {
global $wpdb;
$table = demo_records_table();
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned NOT NULL DEFAULT 0,
title varchar(191) NOT NULL DEFAULT '',
status varchar(20) NOT NULL DEFAULT 'draft',
created_at datetime NOT NULL,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY status (status)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
update_option('demo_records_db_version', '1.0.0');
}
register_activation_hook(__FILE__, 'demo_records_install');
几个必须按这个写法保留的细节:$charset 取自 $wpdb->get_charset_collate(),不要自己写 utf8;主键关键字前是两个空格,PRIMARY KEY (id) 少一个空格 dbDelta 就可能重复建索引;索引统一写 KEY,不要写 INDEX;每个字段单独占一行。varchar(191) 是为了在 utf8mb4 下给字段建索引留出长度余量,如果你的字段不建索引,可以按实际长度写。

激活钩子只在插件启用时触发一次,表结构以后要改,不能靠再启用一次碰运气。加一个版本比对,放在 plugins_loaded 或后台初始化时执行:
function demo_records_maybe_upgrade() {
if (get_option('demo_records_db_version') !== '1.0.0') {
demo_records_install();
}
}
add_action('plugins_loaded', 'demo_records_maybe_upgrade');
改字段时把版本号一起改掉,比如升到 1.0.1,dbDelta 会比对现有表结构,只补差异,不会删除已有数据。把 created_at 写成 datetime NOT NULL,时间值由 PHP 在插入时赋值,可以避开不同 MySQL 版本对零日期默认值的差异。
改完后怎么验证
先在测试站点启用插件,打开 WP_DEBUG,确认没有白屏或数据库报错,再按下面几步核对:
- 读取
$wpdb->last_error,或者在后台临时打印一次,确认为空字符串。有内容说明建表语句本身出错了。 - 执行
SHOW TABLES LIKE 'wp_demo_records';,确认表已存在,前缀要和当前站点的$wpdb->prefix一致。 - 执行
DESCRIBE wp_demo_records;或SHOW INDEX FROM wp_demo_records;,核对字段类型、默认值、索引名是否与预期一致。 - 停用插件再重新启用,观察是否出现表已存在的报错,以及索引有没有被重复添加。正常情况下第二次执行的 dbDelta 应返回空数组。
- 手动插入一条测试记录并查询出来,确认写入和读取路径都通。
预期信号是:表存在、字段和索引与代码一致、$wpdb->last_error 为空、重复启用不报错。dbDelta() 的返回值是一个数组,里面列出新增或修改的字段,调试时可以临时输出它,比只看最终表结构更容易定位格式问题。
容易忽略的边界
- 多站点网络激活:
register_activation_hook在网络激活时通常只对当前站点执行一次,其他子站点的表不会自动创建。需要结合activate_blog钩子,或在每个站点首次加载时做版本检查,具体做法要按你的网络结构确认。 - 权限限制:部分托管环境限制建表和改表权限,dbDelta 静默失败时不一定抛出明显错误,验证阶段必须看
last_error。 - 默认值兼容:
'0000-00-00 00:00:00'这类零日期在较严格的 SQL 模式下可能被拒绝,非必要不要用。 - 索引长度:给
TEXT或超长varchar建索引要指定前缀长度,否则建表会失败。 - 卸载与回退:在
uninstall.php里删表前想清楚,默认建议保留数据;如果表结构改错需要回退,先把旧表改名备份再重建,不要直接 DROP 有数据的表。 - 表名安全:表名只能由固定前缀加固定后缀拼成,不要把用户输入拼进 SQL 语句。
如果验证时发现结构始终不对,可以先注释掉激活钩子,手工在数据库里建一张同结构的表,再对比 dbDelta 生成的差异,通常能很快定位是空格、关键字还是默认值写法的问题。