[关闭]
@AlexZFX 2026-03-02T06:25:51.000000Z 字数 2873 阅读 20

Rename Column 数据丢失的根因分析

问题概述

执行 rename column 操作后,被 rename 的列数据全部变为 NULL。这是 pt-online-schema-change 工具在处理列重命名时的一个已知缺陷,根因在于列映射失败导致数据未被正确复制。

根因分析

pt-osc 的工作原理是:创建新表 → 创建触发器 → 按 chunk 复制数据 → 交换表名。在"复制数据"这一步,工具需要生成如下 SQL:

  1. INSERT INTO new_table (col_new_name, ...) SELECT col_old_name, ... FROM orig_table

关键代码的执行链路如下:

1. 列重命名检测:find_renamed_cols 函数(第10187行)

该函数使用正则表达式来解析 --alter 语句,检测是否有列被重命名:

  1. my $alter_change_col_re = qr/\bCHANGE \s+ (?:COLUMN \s+)?
  2. ($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 哈希。

2. 公共列计算:@common_cols(第9153行)

  1. my @common_cols = map { +{ old => $_, new => $renamed_cols->{$_} || $_ } }
  2. sort { $col_posn->{$a} <=> $col_posn->{$b} }
  3. grep { $new_cols->{$_} || $renamed_cols->{$_} }
  4. 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 过滤掉!

3. 最终效果

  1. # 数据拷贝
  2. my $dml = "INSERT LOW_PRIORITY IGNORE INTO $new_tbl->{name} "
  3. . "(" . join(', ', map { $q->quote($_->{new}) } @common_cols) . ") "
  4. . "SELECT";
  5. my $select = join(', ', map { $q->quote($_->{old}) } @common_cols);

触发场景总结

根据代码分析,数据丢失会在以下场景触发:

flowchart TD A["用户执行 ALTER: RENAME COLUMN old TO new
或 CHANGE COLUMN old new ..."] --> B{"find_renamed_cols
能否识别?"} B -->|"CHANGE COLUMN 语法 ✅"| C["renamed_cols = {old => new}"] B -->|"RENAME COLUMN 语法 ❌"| D["renamed_cols = {} (空)"] C --> E["common_cols 正确映射
{old=>old_name, new=>new_name}"] D --> F["old_name 被 grep 过滤掉
不在 common_cols 中"] E --> G["INSERT INTO new(new_name) SELECT old_name
✅ 数据正确复制"] F --> H["new_name 列未被赋值
❌ 数据变为 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行可以看到:

  1. 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_colsgrep 过滤器会将被 rename 的列排除在外,导致数据拷贝和触发器均不包含该列,新表中该列数据全部为 NULL。

修复建议

find_renamed_cols 函数中增加对 RENAME COLUMN 语法的正则匹配:

  1. # 新增:匹配 MySQL 8.0 的 RENAME COLUMN old TO new 语法
  2. my $alter_rename_col_re = qr/\bRENAME \s+ COLUMN \s+
  3. ($table_ident) \s+ TO \s+ ($table_ident)/ix;
  4. while ( $alter =~ /$alter_rename_col_re/g ) {
  5. my ($orig, $new) = map { $tp->ansi_to_legacy($_) } $1, $2;
  6. next unless $orig && $new;
  7. my (undef, $orig_tbl) = Quoter->split_unquote($orig);
  8. my (undef, $new_tbl) = Quoter->split_unquote($new);
  9. next if lc($orig_tbl) eq lc($new_tbl);
  10. $renames{lc($orig_tbl)} = $new_tbl;
  11. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注