数据库迁移兼容性验收实践:差异来源与差分测试方法
大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。
评估报告上的兼容度只要够高,很多人就敢直接排割接。可这个数字,跟上线之后有没有差异,其实是两回事。栽在这上面的人,我见过太多。
我自己也栽过一次。那是我转行后接的一个迁移项目,前后做了小半年。评估报告给的兼容度是 95%。测试环境跑了两轮,功能全过,评审会上没人提异议。上线第三周,一批跑批数据没按时入账。我查了一整晚。代码没动过,测试用例也覆盖过,问题像是凭空冒出来的。最后定位到一句更新语句。它条件恒不成立,一行都改不了。老库不检查列长度,新库检查。同一句 SQL,一个报错,一个不报错,差别只在约束检查摆在了执行计划的哪个位置。
这不是谁有 bug,是两套规则在边界上的定义不一样。就像以前做设计的时候,还原度 95% 和 99%,用户正常浏览看不出差别,差异都藏在用的时候。兼容性也是这个道理。兼容度只能说明最表面的一层,真正决定上线成败的,是数字背后没被数进去的东西。
那个 95% 到底在数什么
想搞清楚差异,我先回头把那份报告翻开了,想知道那个 95% 是怎么算出来的。评估工具一般做两件事。先把源库里所有对象扫一遍,表、视图、索引、约束、存储过程、函数、触发器、序列,逐个判断目标库能不能理解。再对能转换的那部分做一遍语法试解析。兼容度基本就是可转换对象数除以对象总数。有的工具还会乘一个系数,表示转换完仍需人工改写的比例。
听起来挺严谨,问题出在分母上。它按对象计数,不按权重。一张核心交易表和一个三年没被调用过的存储过程,在分母里一样重。它也衡量不了翻译得对不对,能翻译不等于行为一致。还有一个坑,源库里目标库没有对应物的特性,工具通常单独列成"不支持",这部分直接不进分母。所以兼容度高不等于差异少,它只说明能翻译的东西多。
我现在的做法是把报告导出来重新加权,按对象在生产里的调用频次和业务重要度算。核心链路上的对象权重拉高,几年没跑过的降权。重算出来的数字通常比报告低,但那个数字才跟风险有关。
差异是从哪儿冒出来的
数字重算过,还得回答一个更硬的问题,差异到底从哪来。这几年我踩过的差异,按来源归了六类。归完发现它有规律,不是随机撒开的。
| 差异来源 | 典型表现 | 为什么会不一致 |
|---|---|---|
| 三值逻辑 | 唯一约束里的空值、比较运算的结果、聚合遇到空值 | 空值被定义为"未知",未知等于未知没有确定答案 |
| 类型系统 | 隐式转换优先级、字符串转数字、精度与舍入、长度算字符还是算字节 | 转换规则和长度语义各家定义不同 |
| 求值时机 | 约束检查、触发器、默认值、级联的执行顺序 | 约束是过滤条件还是独立算子,顺序不一样 |
| 会话状态 | 用户变量、会话参数、会话时区、DDL 是否隐式提交 | 会话级状态的范围和生命周期定义不同 |
| 排序与比较 | 空值排最前还是最后、尾随空格是否参与比较、排序是否稳定 | 比较语义依赖字符集与排序规则 |
| 数值与时间 | 定点与浮点的边界、时间戳精度、时区转换、日期算术 | 精度与时区的默认策略不同 |
先拿求值时机说透,开头那句更新语句就出在这类。
-- WHERE 恒不成立,影响行数恒为 0
UPDATE t_order SET remark = '一条明显超过列长度的备注' WHERE 1 <> 1;
-- 一行都没改,还要不要校验 remark 的列长度
如果约束检查在计划里是独立算子,位置摆在过滤之前,零行命中也会报错。如果它挂在"确实产生了一行修改"之后,零行命中就不触发。语法全都正确,报错还是不报错,取决于计划怎么摆。类型系统也不省心,最容易在字段长度上翻车。
CREATE TABLE t_user (name VARCHAR(10));
INSERT INTO t_user VALUES ('数据库小学妹数据库'); -- 10 个汉字
-- 长度按字节算,只能存 3 个汉字;按字符算,10 个才装得下
同一列定义的长度语义,一边按字节,一边按字符,能存的内容差出好几倍。写入被截断还是被拒绝,也都不一样。排序与比较看着最不起眼,出的问题却最隐蔽。
SELECT 'a' = 'a '; -- 尾随空格算不算相等
SELECT * FROM t_order ORDER BY ext_code; -- 空值排最前还是最后
它依赖字符集和排序规则。我见过一个报表系统迁移后数值全对,唯独分页顺序变了。运营第一眼没看出来,直到客户投诉某条记录"找不到"。会话状态我踩得最实在。用户变量看着像程序里的局部变量,写起来方便,实际是会话级的共享状态,值在所有语句之间都可见。并发和并行下,赋值和读取的先后顺序不确定,结果也就不确定。单条测永远测不出来,压测一上量就开始飘。
六类里最反直觉的是三值逻辑。空值不是一个值,它是"未知"。"未知等于未知"的结论既不是真也不是假,是第三种状态。唯一约束的判定是,任意两行键值一旦被判为相等就拒绝。两个空值比出来是第三种状态,不是真,约束于是不拒绝。这就是允许为空的列上,唯一约束拦不住重复的原因。它不是某个实现的怪癖,是标准本身在边界上留的口子。
靠人想用例,永远想不全
知道差异从哪来了,还得有办法把它们捞出来。靠人工想用例有两个毛病。想不全,而且不可回归。同一个差异点这次发现了,下次换版本还得重新想一遍。
我后来改成差分测试。思路很简单。同一批用例,让它两边都跑,逐条比对结果,差异自动落出来。用例库靠两个来源撑着,缺一不可。一个是真实流量。从生产或预发录一批 SQL,脱敏之后回放,覆盖的是真实的调用分布,跑出来的差异是业务现在就会遇到的。另一个是按上面那六类来源手工构造的边界集,覆盖的是真实的差异分布,跑出来的差异是业务迟早会遇到的。两类差异性质不同,处理优先级也不同。
每发现一个差异,就把它固化成一条回归用例,同时记下两边各自的期望结果。下次升级版本、打补丁,把这套跑一遍就行。用例库会随着时间变厚,攒下来的东西一直能用,越攒越省事。覆盖率不要按用例条数算,按六个来源算。每个来源都得有用例,而且都得有确实命中过差异的记录。某一维从头到尾一条差异都没探到,通常不是因为它干净,是用例还不够极端。
验收到底该投多少
不是所有系统都值得验到同一个深度。全验最安全,但人力不允许,也没必要。我按两个维度分投入。一个是这个系统在业务链路上有多重要,另一个是它的 SQL 复杂度离差异有多近。
边缘系统、只读报表,验到语法加基础语义,再补一份性能基线就够。核心交易,或者带着大量存储过程迁移的,必须走到末梢语义和差分测试。这里没商量余地。顺序上,我先看它的 SQL 复杂度分布,再看有没有批量任务。复杂度高又有批量的系统,差异的暴露面是相乘放大的,不是相加。带批量的还得额外盯跑批窗口。单条 SQL 快不算数,窗口关不上就是不过。
避坑清单
兼容度是工具算出来的,业务没验过,它只能说明最表面那层。别把这个数字当成割接的通过标准,尤其别拿它去汇报风险。
末梢语义的用例要按边界写,不能按正常流程写。同一句 SQL,正常的参数永远碰不到那个分界点。我现在的做法是让业务方先列一份清单。清单上只放"最不可能发生,但一旦发生就麻烦"的场景,从里面挑用例。
最后一条是我自己搞错的。我以前验收只看数据条数,对得上就签字。后来才知道条数对得上不代表内容对得上。金额被截断了、状态映射错了,条数照样一条不差。现在我至少再加一步字段级的值比对,宁可多跑一遍。
写在最后
兼容性验收该在哪儿停,取决于你能承受多大的一致性风险。这件事跟工具给你的信心关系不大,跟你对业务的了解关系很大。信创替换这几年推得快,很多要迁的系统,源库和目标库连设计哲学都不一样,差异只会更多,不会更少。
我这两年最大的转变,是不再问"兼容度多少",改问三个更笨的问题。这个差异会在什么场景下出现。出现了业务能不能扛住。我有没有一条用例真的把它跑出来过。工具能帮你把差异列出来,留还是不留,得人来做。
你手上的迁移项目,兼容性验收到了哪一层?又漏过哪些边界上的坑?评论区聊聊。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋
- 点赞
- 收藏
- 关注作者
评论(0)