建表时 `dbDelta` 的"空格洁癖"让我丢了三个索引:从 `KEY` 语法到字段排序的隐性规则
上周帮同事排查一个诡异问题:插件激活后表是建出来了,但三个索引全没生效。`EXPLAIN` 一看全是 `ALL` 全表扫描,后台查询直接爆炸。最后发现是 `dbDelta` 对空格的敏感度远超预期——不是"有没有空格",而是"空格出现在哪"。
先贴个最小复现。下面这段 SQL 在 phpMyAdmin 里跑毫无问题,`dbDelta` 却静默跳过索引创建:
CREATE TABLE `{$wpdb->prefix}my_logs` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) unsigned NOT NULL DEFAULT '0',
`action` varchar(50) NOT NULL DEFAULT '',
`created_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
KEY `user_action` (`user_id`,`action`),
KEY `created_lookup` (`created_at`)
) ENGINE=InnoDB {$wpdb->get_charset_collate()};
问题藏在 `KEY` 声明的格式里。dbDelta 的正则匹配要求 KEY 后面必须紧跟恰好一个空格,然后是索引名。如果你写成 KEY user_action(两个空格)或者 KEY `user_action`(带反引号),它直接认不出来,不会报错,只是默默不建索引。
更坑的是字段排序。我习惯把 PRIMARY KEY 放最后,但 dbDelta 的对比逻辑是按"期望 SQL"的顺序来校验现有表结构的。如果你的升级 SQL 里字段顺序和当前表不一致,它会认为需要重建——而重建过程中如果新索引语法有问题,老索引先被删、新索引建失败,结果就是索引丢失。
现在我的建表 SQL 必须过三道检查:
1. 空格标准化
写了个小函数预处理,把多个空格压成一个,去掉多余的反引号包裹(索引名不需要):
function normalize_dbdelta_sql( $sql ) {
// KEY 后面必须单空格,索引名不加反引号
$sql = preg_replace( '/KEY\s+`?(\w+)`?\s*\(/', 'KEY $1 (', $sql );
// 多个空格合并
$sql = preg_replace( '/\s+/', ' ', $sql );
return trim( $sql );
}
2. 升级时的"索引保险"
别光依赖 dbDelta 的返回值。我在升级钩子末尾手动校验关键索引是否存在,没有就补建:
function ensure_indexes_exist() {
global $wpdb;
$table = $wpdb->prefix . 'my_logs';
$expected = [ 'user_action', 'created_lookup' ];
$existing = $wpdb->get_results( "SHOW INDEX FROM {$table}", ARRAY_A );
$existing_names = wp_list_pluck( $existing, 'Key_name' );
foreach ( $expected as $index ) {
if ( ! in_array( $index, $existing_names, true ) ) {
// 补建,同时写错误日志
$wpdb->query( "ALTER TABLE {$table} ADD INDEX {$index} (...)" );
error_log( "Index {$index} was missing and recreated" );
}
}
}
3. 卸载时的"索引依赖"陷阱
这个反直觉:如果你插件 A 建了表,插件 B 通过 ALTER TABLE 加了索引,你 A 的卸载逻辑里直接 DROP TABLE IF EXISTS,B 的索引跟着陪葬,B 下次查询直接炸。
我的做法是卸载前检查表是否"干净"——只有本插件创建的索引才允许连带删除:
// uninstall.php
$prefix = 'myplugin_'; // 本插件索引命名前缀
$indexes = $wpdb->get_results( "SHOW INDEX FROM {$table}" );
foreach ( $indexes as $index ) {
if ( 0 === strpos( $index->Key_name, $prefix ) ) {
// 安全的,可以 DROP TABLE
} else {
// 存在外部索引,改走清理数据而非删表
$safe_to_drop = false;
break;
}
}
最后说个血泪教训:某次升级我把 datetime 改成了 timestamp,dbDelta 没检测到变化(它不认这种"同义替换"),结果新老数据混存,时区处理全乱。现在凡是涉及字段类型变更,我直接走 ALTER TABLE 显式迁移,不再赌 dbDelta 的"智能"。
你们有没有被 dbDelta 的"沉默"坑过?我好奇它到底还有多少隐性规则没写在文档里。

