@AlexZFX
2026-03-02T06:25:51.000000Z
字数 2873
阅读 20
执行 rename column 操作后,被 rename 的列数据全部变为 NULL。这是 pt-online-schema-change 工具在处理列重命名时的一个已知缺陷,根因在于列映射失败导致数据未被正确复制。
pt-osc 的工作原理是:创建新表 → 创建触发器 → 按 chunk 复制数据 → 交换表名。在"复制数据"这一步,工具需要生成如下 SQL:
INSERT INTO new_table (col_new_name, ...) SELECT col_old_name, ... FROM orig_table
关键代码的执行链路如下:
find_renamed_cols 函数(第10187行)该函数使用正则表达式来解析 --alter 语句,检测是否有列被重命名:
my $alter_change_col_re = qr/\bCHANGE \s+ (?:COLUMN \s+)?($table_ident) \s+ ($table_ident)/ix;
关键问题:这个正则只匹配 CHANGE [COLUMN] old_name new_name 语法!
如果用户使用的是 MySQL 8.0 引入的 RENAME COLUMN old_name TO new_name 语法,该正则完全无法匹配,find_renamed_cols 会返回空的 %renames 哈希。
@common_cols(第9153行)
my @common_cols = map { +{ old => $_, new => $renamed_cols->{$_} || $_ } }sort { $col_posn->{$a} <=> $col_posn->{$b} }grep { $new_cols->{$_} || $renamed_cols->{$_} }keys %$orig_cols;
这段代码的逻辑是:
1. 遍历原表所有列(keys %$orig_cols)
2. 过滤条件:该列在新表中存在($new_cols->{$_})或者在 renamed_cols 中有记录($renamed_cols->{$_})
3. 映射:如果该列在 renamed_cols 中有记录,就用新名字;否则用原名字
当 $renamed_cols 为空时(即未检测到重命名),被 rename 的列会因为老名字在新表中不存在($new_cols->{old_name} 为 false),直接被 grep 过滤掉!
@common_cols 中
# 数据拷贝my $dml = "INSERT LOW_PRIORITY IGNORE INTO $new_tbl->{name} ". "(" . join(', ', map { $q->quote($_->{new}) } @common_cols) . ") ". "SELECT";my $select = join(', ', map { $q->quote($_->{old}) } @common_cols);
NULL)根据代码分析,数据丢失会在以下场景触发:
| 场景 | 是否丢数据 | 原因 |
|---|---|---|
CHANGE COLUMN old new INT |
❌ 不丢 | 正则能匹配,正确映射 |
RENAME COLUMN old TO new (MySQL 8.0) |
✅ 丢数据 | 正则无法匹配,列被跳过 |
--no-check-alter + CHANGE COLUMN |
❌ 不丢 | --no-check-alter 只影响 check_alter 警告校验,不影响 find_renamed_cols 的调用 |
--no-check-alter 的使用从 ptexcutethread.cpp 第70行可以看到:
std::string check_alter = m_pt_param->getCheckAlter() ? "" : " --no-check-alter ";
如果 ZK 配置中 check_alter 为 "0",则 getCheckAlter() 返回 false,命令行会加上 --no-check-alter。这意味着即使使用了 CHANGE COLUMN 语法且被正确检测到,pt-osc 也不会发出警告就直接执行。但注意:--no-check-alter 并不影响 find_renamed_cols 的调用,它只是跳过了 check_alter 中的安全检查(第10102行)。所以这个参数本身不是数据丢失的直接原因。
根因:find_renamed_cols 函数(第10187行)中的正则表达式 $alter_change_col_re 只能识别 CHANGE [COLUMN] old new 语法,无法识别 MySQL 8.0 的 RENAME COLUMN old TO new 语法。当用户使用后者时,renamed_cols 为空,@common_cols 的 grep 过滤器会将被 rename 的列排除在外,导致数据拷贝和触发器均不包含该列,新表中该列数据全部为 NULL。
在 find_renamed_cols 函数中增加对 RENAME COLUMN 语法的正则匹配:
# 新增:匹配 MySQL 8.0 的 RENAME COLUMN old TO new 语法my $alter_rename_col_re = qr/\bRENAME \s+ COLUMN \s+($table_ident) \s+ TO \s+ ($table_ident)/ix;while ( $alter =~ /$alter_rename_col_re/g ) {my ($orig, $new) = map { $tp->ansi_to_legacy($_) } $1, $2;next unless $orig && $new;my (undef, $orig_tbl) = Quoter->split_unquote($orig);my (undef, $new_tbl) = Quoter->split_unquote($new);next if lc($orig_tbl) eq lc($new_tbl);$renames{lc($orig_tbl)} = $new_tbl;}