Oracle 到 MySQL 迁移复盘:自增主键、时间类型和空值才是真正的坑
一次 Oracle 11g 到 MySQL 8.0 的生产迁移复盘:200+ 张表、约 3 亿行数据。迁移本身两天跑完,踩坑和修复多花了一周。问题不在导数据本身,而在序列转自增、DATE 含时间被截断、Oracle 空串等于 NULL 这三个认知差。
背景
项目是从 Oracle 11g(AL32UTF8)迁移到 MySQL 8.0(utf8mb4),表结构中混用了序列自增和业务主键,时间字段全是 DATE 类型(Oracle 的 DATE 实际包含时分秒),部分旧模块仍在使用空串 '' 表示"无值"。迁移不能停服超过 4 小时,之后需要 CDC 实时追增量。
方案怎么定
迁移分两个阶段:窗口期全量,窗口后增量追平再切读。
| 条件 | 选择 | 原因 |
|---|---|---|
| 窗口期全量 | 全量 | 一次性覆盖全部 200+ 张表 |
| 夜间低峰追增量 | 增量字段(UPDATE_TIME) | 表上没有统一的 binlog 消费链路,CDC 需要额外开日志且审批周期长 |
| 后续实时同步 | CDC | 增量字段不能捕获 DELETE,最终还是要上 CDC |
最终组合:全量 + 增量字段追平 → CDC 持续同步。全量用一种工具,CDC 另起一种,不如一个工具三种模式全支持,省掉来回切配置的成本。
真正卡住的地方
整轮迁移里真正卡住时间的不是数据量大,是下面三个 Oracle 独有的行为差异。
坑一:序列转自增,主键值没跟上
Oracle 用 SEQUENCE 生成主键,很多表的自增逻辑写在触发器里。MySQL 只有 AUTO_INCREMENT,不会自动建序列对象。
- 现象:自动建表完成后,目标表没有自增属性,写入时报主键冲突。
- 处理:建表后手动检查所有源端有序列的表,在 MySQL 侧设置
AUTO_INCREMENT = 当前最大值 + 1。触发器逻辑需要在前端代码或 DataMover 的转换表达式中补。 - 教训:自动建表只建字段结构,不建序列。迁移前必须逐表列出哪些表依赖序列,批量
ALTER TABLE ... AUTO_INCREMENT = N。
坑二:Oracle DATE 迁移到 MySQL DATE 被截断时分秒
Oracle 的 DATE 类型精确到秒,MySQL 的 DATE 只有年月日。
- 现象:全量跑完,目标端所有
DATE字段只有日期,时分秒变成00:00:00。 - 排查:自动建表时源端
DATE→ 目标端也是DATE,但因为类型名相同,自动映射没提示。 - 处理:下掉目标表,重新配置映射,把所有 Oracle
DATE列手动改成 MySQLDATETIME。重新跑全量。 - 教训:Oracle
DATE≠ MySQLDATE。字段映射页面不能只看类型名相同就跳过,涉及时间的列必须逐列点开确认目标类型是DATETIME还是TIMESTAMP。
坑三:Oracle 空串等于 NULL,MySQL 里它们是两回事
Oracle 会把 '' 自动当成 NULL 存储。MySQL 不会,'' 就是一个空字符串。
- 现象:MySQL 目标表里
NOT NULL列写入失败,报Column 'xxx' cannot be null。去 Oracle 里查,那列明明有值——但值是''空串,在 Oracle 里不算 NULL。 - 处理:在字段映射中给这些列补
default()函数:当源端值为空串时设为目标端默认值。核心业务列走人工清洗——把 Oracle 的空串统一刷成业务默认值后再迁移。 - 教训:迁移前先跑一遍
SELECT col, COUNT(*) FROM t WHERE col = ''和WHERE col IS NULL分别统计,两边的差值就是 Oracle 空串但 MySQL 当 NULL 的量。
对比:这几次迁移分别踩的坑
| 踩坑点 | Oracle → MySQL | MySQL → 达梦 | SQL Server → MySQL |
|---|---|---|---|
| 自增/序列 | SEQUENCE 不存在,需手动设 AUTO_INCREMENT | 自增→IDENTITY,注释需检查 | identity 转 AUTO_INCREMENT,种子值需校对 |
| 时间 | DATE 含时分秒,需改 DATETIME | DATETIME 精度差异 | datetime2 精度 7 位,MySQL 只到 6 位 |
| 空值 | 空串 = NULL 的行为差异 | 空串处理一致 | 空串处理一致 |
跟 MySQL→达梦 那次比,Oracle 迁移多了一个时间类型语义鸿沟和一个空串的认知差。跟 SQL Server→MySQL 那次比,Oracle 的序列问题又比 identity 转 AUTO_INCREMENT 难处理——identity 是列的属性,序列是独立对象,耦合在触发器和代码里。
用 DataMover 怎么落地
Oracle 端准备好只读账号,MySQL 端准备写入账号。DataMover 以 Docker 方式部署,Manager 在 8000 端口,Worker 在 8011。
- 配源端:新增 Oracle 数据源,填 SID/Service Name、主机端口、只读账号。
连接测试通过后再进下一步。 - 配目标端:新增 MySQL 数据源,填主机端口、写入账号、目标库名。
- 创建普通任务:选择 Oracle 为源,MySQL 为目标。全量阶段就选普通任务。
- 选表:从源端树结构中勾选需要迁移的表,支持单选和批量勾选。
- 检查字段映射:这里是整轮迁移最关键的一步。DataMover 会自动匹配同名字段,但需要手动逐表确认:
- 所有 Oracle
DATE列的目标类型改为DATETIME。 - 源端
NUMBER列确认目标精度:NUMBER(*)默认映射到DECIMAL(38,9),纯整数字段建议收窄到BIGINT。 VARCHAR2(4000)超出 MySQL 行大小限制时,考虑降为TEXT。- 有空串风险的非空列,在
exp列中配置default()。
- 所有 Oracle
- 配置同步策略:全量任务选择"全量同步",设置合适的批大小(大数据量表建议 2000-5000,不要盲目调大)。目标表策略选择"覆盖"或"追加",窗口期迁移建议先清空目标表再用"追加"。
- 启动任务:选择执行 Worker,点击执行。Manager 下发任务,Worker 并行读取 Oracle 和写入 MySQL。
- 监控和日志:在执行记录中查看进度、行数和错误日志。日志里会列出所有字段类型转换警告和写入错误。
校验和返工
全量跑完后,用下面这组 SQL 做校验:
-- 1. 总行数校验
SELECT COUNT(*) FROM source_table;
SELECT COUNT(*) FROM target_table;
-- 2. 时间范围校验
SELECT MIN(create_time), MAX(create_time) FROM source_table;
SELECT MIN(create_time), MAX(create_time) FROM target_table;
-- 3. 关键字段聚合校验
SELECT status, COUNT(*) FROM source_table GROUP BY status ORDER BY status;
SELECT status, COUNT(*) FROM target_table GROUP BY status ORDER BY status;
-- 4. 抽样逐列对比
SELECT * FROM source_table WHERE id IN (100, 5000, 100000);
SELECT * FROM target_table WHERE id IN (100, 5000, 100000);
-- 5. 空串专项检查
SELECT COUNT(*) FROM source_table WHERE remark = '';
SELECT COUNT(*) FROM target_table WHERE remark = '';
校验不过的就查日志,修正字段映射后重跑该表,或者单独建一个增量任务只拉错掉的区间。
复盘结论
- 先验类型再跑全量:迁移前把每张表的字段类型导成 Excel,逐个标记 Oracle 类型 → 目标类型 → 人工检查项,再用自动映射的结果对照。不要靠跑完全量再发现类型错误。
- 序列不是建表的一部分:自动建表工具不迁移序列。提前列好序列清单,全量完成后批量
ALTER TABLE ... AUTO_INCREMENT = N。 - Oracle DATE 在 MySQL 里永远是 DATETIME:没有例外。Oracle 没有独立的
DATETIME类型,所有带时间的列都在DATE里。迁移时必须手动转。 - 空串问题是数据库中立的陷阱:开发在 Oracle 里习惯写
SET col = '',DBA 在 Oracle 里COUNT(*) WHERE col IS NULL会包括空串。到了 MySQL,这两个查询结果不一样。上线前手写查询验证。 - 增量字段追平只是过渡:增量字段不能捕获 DELETE,也无法保证 UPDATE 一定更新了增量字段。最终还是要上 CDC。选工具的时候先确认 CDC 支持情况,省得中间又切一套工具。
FAQ
Oracle 的 NUMBER(*) 迁移到 MySQL 用什么类型?
DataMover 默认映射到 DECIMAL(38,9)。如果该列实际只用整数值(如主键、状态码),手动改为 BIGINT。如果确实需要超过 18 位精度的大数,保持 DECIMAL 但注意 MySQL 的 DECIMAL 最大 65 位。
Oracle 到 MySQL 迁移支持 CDC 实时同步吗?
支持。DataMover 基于 Debezium 实现 Oracle CDC,需要源端开启 Archive Log 并配置 LogMiner 权限。社区版就支持 CDC,不限制数据量。但 CDC 快照策略需谨慎选择——目标表已有数据时不要选"清空目标表"。
全量迁移对 Oracle 源库性能影响多大?
DataMover 采用无锁读取方式,对源库性能影响极小。建议在业务低峰期执行全量迁移,并控制 Worker 的批大小和并发数,避免目标 MySQL 写入压力过大。
迁移完以后还需要核对什么?
除了行数和抽样对比,还必须核对:金额字段的合计值、时间字段的时区偏移、枚举字段的取值范围和计数、NULL 列的行数和比例。行数一致不表示数据一致。
下一步
Oracle → MySQL 只是去 Oracle 化的第一步。如果目标端未来还要迁到达梦、GaussDB 等国产库,核心经验可以复用——序列转自增、时间类型核对、空串检查这三件事换什么目标库都不会过时。
- 下载 DataMover:https://datamover.cn/download.html
- 使用文档:https://datamover.cn/doc/
- 相关方案:Oracle 到 达梦 国产化替代完整方案
- 相关方案:MySQL 到达梦数据库迁移同步
- 相关方案:SQL Server 到 MySQL 数据迁移同步
相关同步方案
除了Oracle到MySQL数据迁移同步完整方案,DataMover还支持以下场景:
常见问题解答
Oracle的DATE迁移到MySQL用什么类型?
Oracle的DATE包含时分秒,MySQL的DATE只有年月日。迁移时必须将Oracle DATE改为MySQL DATETIME或TIMESTAMP,自动建表工具不会自动区分这个差异。
Oracle的序列SEQUENCE怎么迁移到MySQL?
MySQL没有SEQUENCE对象,需使用AUTO_INCREMENT替代。迁移完成后需手动执行ALTER TABLE设置起始值,并检查依赖序列的触发器和代码逻辑。
Oracle到MySQL的CDC实时同步支持吗?
支持。DataMover基于Debezium实现Oracle CDC,需要源端开启Archive Log。社区版就支持CDC。
Oracle的空串迁移到MySQL会出问题吗?
Oracle中空字符串等同于NULL,MySQL中空字符串和NULL是不同的。有NOT NULL约束的列可能在写入时报错,需在字段映射中配置默认值或清洗源端数据。
开始配置 DataMover 同步任务
下载 DataMover 后,可按文档完成数据源、表映射、同步策略、任务启动和执行结果校验。