Oracle 到 OceanBase 信创迁移整体方法论
1. 对应简历段落
这篇文章对应简历中“参与核心业务系统信创改造,负责 Oracle 到 OceanBase 迁移、SQL 兼容性改造、存储过程与定时任务 Java 化、分页与树查询改造、新旧 SQL 结果对比工具建设、慢 SQL 优化和上线验证”等经历。
面试里这段经历通常会被追问得很细。因为它不是简单“把数据库换一下”,而是同时涉及数据库语法、执行计划、事务一致性、批处理调度、MyBatis 插件、数据校验、灰度上线和生产性能治理。真正能讲清楚的人,需要说明三个层次:
第一,为什么要迁移。也就是信创背景、国产数据库替换、运维标准化、成本和风险控制。
第二,怎么迁移。包括迁移评估、对象梳理、SQL 扫描、兼容性改造、数据同步、双跑验证、灰度切换和回退预案。
第三,迁移后怎么稳定运行。包括慢 SQL 优化、OceanBase 执行计划分析、索引调整、分页策略改造、批任务改造、监控告警和问题复盘。
如果面试官问“你在 Oracle 到 OceanBase 迁移中具体做了什么”,不能只回答“改了 SQL”。更好的表达是:我负责应用侧和数据库侧之间的兼容性治理,把 Oracle 方言、存储过程、定时任务和慢 SQL 按风险分层处理,并通过工具化手段做新旧结果对比和性能回归,最终让迁移不是一次性人工改造,而是一套可重复执行的方法论。
2. 业务背景
业务系统可以理解为保险销售、客户经营和交易管理类的核心系统。系统历史比较长,早期以 Oracle 为主,Java 应用层使用 Spring、MyBatis,部分复杂逻辑沉淀在数据库里,比如存储过程、函数、定时作业、报表 SQL、客户层级查询和月底批处理。
这种系统的典型特点是“业务稳定但技术债重”:
- SQL 数量多,很多 SQL 写法强依赖 Oracle。
- 报表查询复杂,存在大量嵌套子查询、
ROWNUM分页、NVL、DECODE、TO_DATE、TRUNC等函数。 - 组织架构、客户层级、代理人团队等数据经常使用
CONNECT BY PRIOR做树查询。 - 批处理逻辑有一部分在 OGG、存储过程、数据库 Job 或调度脚本里,应用侧对这些逻辑感知不足。
- 空字符串和
NULL在 Oracle 中语义特殊,迁移后可能影响查询条件、唯一约束、字段默认值和 Java 判空逻辑。
信创迁移的目标不是单纯追求“数据库国产化”这个动作,而是在不影响现有业务连续性的前提下,把底层数据库从 Oracle 迁移到 OceanBase,并尽量减少对上层业务的扰动。对于保险销售系统来说,迁移窗口通常非常敏感:白天有出单、保全、客户跟进、活动转化,晚上有批处理、报表和对账。任何一个 SQL 结果不一致、批任务漏跑或慢查询放大,都可能影响业务人员第二天的使用。
所以这个项目的核心矛盾是:既要完成底层数据库替换,又要保证业务结果、性能表现和运维方式可控。这里的难点不是单点技术,而是把一批看似零散的问题组织成工程化迁移流程。
3. 迁移阶段
Oracle 到 OceanBase 的迁移可以分成六个阶段:资产盘点、兼容性评估、改造实施、数据同步、双跑验证、切换优化。
第一阶段是资产盘点。要先回答“系统到底用了哪些 Oracle 能力”。盘点对象不只包括表和索引,还要包括视图、序列、同义词、触发器、存储过程、函数、包、Job、物化视图、DBLink、分区表、复杂 SQL 和应用配置。应用侧还要扫描 Mapper XML、注解 SQL、动态 SQL、分页插件、自定义 TypeHandler、批处理脚本和报表模板。
第二阶段是兼容性评估。把 SQL 按风险分层:普通 CRUD 属于低风险;分页、日期函数、字符串函数、序列、批量插入属于中风险;CONNECT BY、复杂报表、存储过程、动态拼接 SQL、跨库查询、触发器属于高风险。高风险部分不能靠全局替换解决,必须逐条设计改造方式。
第三阶段是改造实施。这里既有 SQL 层面的改造,也有架构层面的改造。例如 ROWNUM 改成 LIMIT/OFFSET,CONNECT BY 改成递归 CTE,Oracle 函数改成 OceanBase 兼容写法,存储过程改成 Java Service + XXL-Job,OGG 同步逻辑改成更清晰的数据同步或任务编排逻辑。应用侧需要处理 MyBatis 分页拦截器、数据库方言判断、SQL 生成策略和参数绑定。
第四阶段是数据同步。生产迁移通常不能停机很久,因此需要考虑全量导入和增量同步。全量阶段保证表结构、基础数据和历史数据完整;增量阶段保证迁移窗口内 Oracle 侧新增和变更的数据能同步到 OceanBase。对核心交易表,要特别关注主键、序列、时间戳、状态字段和幂等标识。
第五阶段是双跑验证。不能只验证表行数,要验证业务结果。比如同一个客户列表查询、同一个销售团队层级、同一张日报、同一个保单统计口径,在 Oracle 和 OceanBase 上执行后结果是否一致。对于金额、日期、排序、分页、空字段,需要做更细粒度对比。
第六阶段是切换优化。灰度切换后,要持续观察慢 SQL、连接池、事务耗时、锁等待、CPU、内存、OceanBase 租户资源、SQL 执行计划和索引命中情况。迁移上线不是终点,真正的验收是生产流量跑过高峰期、月底批处理和核心报表周期之后,系统仍然稳定。
4. 兼容性问题
兼容性问题是迁移中最容易被低估的部分。很多 SQL 在语法上能执行,但语义已经变了;有些 SQL 在测试数据下没问题,在生产数据量下会变成慢 SQL。
4.1 ROWNUM 到 LIMIT
Oracle 常见分页写法如下:
SELECT *
FROM (
SELECT t.*, ROWNUM rn
FROM (
SELECT * FROM policy_order ORDER BY create_time DESC
) t
WHERE ROWNUM <= :endRow
)
WHERE rn > :startRow;
迁移到 OceanBase MySQL 模式时,通常改成:
SELECT *
FROM policy_order
ORDER BY create_time DESC
LIMIT :offset, :pageSize;
这里最重要的不是语法替换,而是分页语义。Oracle 的 ROWNUM 是在结果返回过程中生成的,如果没有把 ORDER BY 放在内层子查询里,很容易先截断再排序,导致分页结果错误。迁移时要检查原 SQL 是否本来就有语义问题,不能机械替换。
另外,深分页会带来性能问题。LIMIT 100000, 20 需要扫描并丢弃大量行。对于客户列表、保单列表这种高频页面,改造时要结合业务场景考虑游标分页或基于索引的“seek pagination”:
SELECT *
FROM policy_order
WHERE create_time < :lastCreateTime
ORDER BY create_time DESC
LIMIT :pageSize;
这种方式要求排序字段稳定,最好组合主键避免同一时间戳导致翻页重复或遗漏:
WHERE (create_time < :lastCreateTime)
OR (create_time = :lastCreateTime AND id < :lastId)
ORDER BY create_time DESC, id DESC
LIMIT :pageSize;
4.2 CONNECT BY 到递归 CTE
Oracle 中组织树、客户归属树、代理人团队树常用 CONNECT BY:
SELECT org_id, parent_org_id, org_name, LEVEL
FROM sales_org
START WITH org_id = :rootOrgId
CONNECT BY PRIOR org_id = parent_org_id;
迁移后可以改成递归 CTE:
WITH RECURSIVE org_tree AS (
SELECT org_id, parent_org_id, org_name, 1 AS level
FROM sales_org
WHERE org_id = :rootOrgId
UNION ALL
SELECT c.org_id, c.parent_org_id, c.org_name, p.level + 1
FROM sales_org c
JOIN org_tree p ON c.parent_org_id = p.org_id
)
SELECT *
FROM org_tree;
这里要重点关注四个问题。
第一,方向不能写反。CONNECT BY PRIOR org_id = parent_org_id 表示从父节点找子节点;如果改成 CTE 时 join 条件写反,会变成向上找父链。
第二,层级字段要自己生成。Oracle 的 LEVEL 是内置伪列,递归 CTE 里需要 level + 1。
第三,要处理环。生产组织数据偶尔会因为脏数据出现 A 的父级是 B,B 的父级又回到 A。Oracle 可以使用 NOCYCLE,CTE 中则需要加深度限制或路径判断。
第四,排序语义不同。Oracle 的树查询可以配合 ORDER SIBLINGS BY,递归 CTE 默认不保证兄弟节点顺序。如果前端依赖树展示顺序,要补充排序字段,比如 sort_no、path。
4.3 空字符串问题
Oracle 中空字符串 '' 会被当成 NULL 处理,而 MySQL 模式下的 OceanBase 通常会区分空字符串和 NULL。这会带来很多隐蔽问题。
例如 Oracle 中:
WHERE mobile IS NULL
可能同时匹配原来写入的 NULL 和空字符串。但迁移后,如果数据里存在 '',这个条件只匹配 NULL,业务结果就变了。
应用层也会受影响。比如 Java 代码里:
if (customer.getEmail() == null) {
// 未填写邮箱
}
迁移后如果数据库返回的是空字符串,这段逻辑就不会进入。保险客户资料、证件号、推荐人编号、备注字段都可能踩到这个坑。
处理方式一般有三种:
- 数据治理:迁移前统一把空字符串清洗为
NULL,或按字段规则统一为''。 - SQL 兼容:查询条件写成
field IS NULL OR field = '',但不能滥用,否则影响索引。 - 应用统一:在 DTO、TypeHandler 或业务工具类里统一处理 blank 字符串。
更稳妥的方式是按字段分类。对于手机号、证件号、邮箱这类有明确“未填写”语义的字段,可以统一为 NULL;对于备注、扩展字段、JSON 文本字段,则要看历史业务是否区分空和未填。
4.4 函数差异
Oracle 函数差异很多,迁移时要建立函数映射表,而不是边改边查。常见改造包括:
NVL(a, b) -> IFNULL(a, b) 或 COALESCE(a, b)
DECODE(x, a, b, c) -> CASE WHEN x = a THEN b ELSE c END
SYSDATE -> NOW() 或 CURRENT_TIMESTAMP
TO_DATE -> STR_TO_DATE 或显式时间参数绑定
TO_CHAR(date) -> DATE_FORMAT
TRUNC(date) -> DATE 或按时间范围改写
SUBSTR -> SUBSTRING
INSTR -> LOCATE 或 INSTR,视模式而定
NVL2 -> CASE WHEN expr IS NOT NULL THEN v1 ELSE v2 END
这里最容易出问题的是日期函数。很多 Oracle SQL 喜欢写:
WHERE TRUNC(create_time) = TO_DATE(:bizDate, 'yyyy-mm-dd')
迁移时不建议简单改成对字段套 DATE(create_time),因为这样会让索引失效。更好的写法是范围查询:
WHERE create_time >= :startTime
AND create_time < :endTime
金额和字符串拼接也要小心。Oracle 中字符串拼接用 ||,MySQL 模式常用 CONCAT。如果拼接字段可能为 NULL,结果也可能与原来不同,需要结合 COALESCE 保持语义。
4.5 序列、自增与主键
Oracle 常用 Sequence:
SELECT seq_policy_order.NEXTVAL FROM dual;
迁移到 OceanBase 后,可以根据系统策略选择自增主键、分布式 ID、号段模式或兼容序列。对于金融类系统,不建议临时混用多种主键生成方式。尤其在双写或增量同步阶段,如果 Oracle 和 OceanBase 同时生成主键,就必须避免冲突。
如果应用已经有雪花 ID 或号段服务,可以逐步把数据库序列收敛到应用层;如果暂时不能改动,则要把序列作为数据库对象同步和验证的一部分,确保起始值大于历史最大值。
5. 工具化改造
靠人工逐条改 SQL 是不可控的。迁移中应该尽量把重复动作工具化,至少包括 SQL 扫描、分页改造、函数检测、新旧 SQL 对比、慢 SQL 采集和结果差异报告。
5.1 MyBatis 分页拦截器
很多老系统中分页散落在 Mapper XML 里,有的用 ROWNUM,有的在 Java 代码里拼接,有的依赖 PageHelper 或自研分页工具。迁移时可以在 MyBatis 插件层统一处理一部分低风险分页 SQL。
MyBatis 拦截器一般拦截 StatementHandler.prepare 或 Executor 查询链路,拿到原始 SQL、分页参数和数据库方言,再生成目标分页 SQL。简化逻辑如下:
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
PageRequest page = PageContext.get();
if (page == null) {
return invocation.proceed();
}
String sql = boundSql.getSql();
String pageSql = dialect.toPageSql(sql, page.getOffset(), page.getPageSize());
MetaObject metaObject = SystemMetaObject.forObject(boundSql);
metaObject.setValue("sql", pageSql);
return invocation.proceed();
}
这个方案的边界也要讲清楚。分页拦截器适合统一处理简单列表查询,但不适合盲目改造所有复杂 SQL。对于已经包含 ROWNUM、GROUP BY、UNION、窗口函数、复杂排序的 SQL,要进入人工白名单或专项改造。否则很容易出现 count SQL 错误、排序丢失或参数绑定错位。
5.2 OGG/存储过程改 Java + XXL-Job
很多 Oracle 老系统会把批处理逻辑放在存储过程、数据库 Job 或 OGG 同步链路中。迁移到 OceanBase 时,可以顺势把一部分数据库内逻辑改到 Java 应用层,再由 XXL-Job 统一调度。
这么做的原因不是“存储过程不好”,而是出于可维护性和可观测性:
- Java 代码更容易做单元测试、代码评审和版本管理。
- XXL-Job 有任务日志、失败重试、分片执行、告警和手工触发能力。
- 业务逻辑放在应用层后,更容易接入统一监控和链路追踪。
- 批任务可以通过幂等表、任务流水表控制重入和补偿。
例如原来一个存储过程负责每天汇总销售团队业绩:
BEGIN
pkg_sales_stat.refresh_team_daily_stat(p_biz_date => :bizDate);
END;
改造后可以拆成 Java 任务:
@XxlJob("refreshTeamDailyStatJob")
public void refreshTeamDailyStat() {
LocalDate bizDate = jobParamParser.parseDateOrYesterday();
String taskKey = "TEAM_DAILY_STAT:" + bizDate;
if (!taskLock.tryLock(taskKey)) {
return;
}
statService.deleteOldStat(bizDate);
statService.calculateAndInsert(bizDate);
statService.markSuccess(taskKey);
}
这里的重点是幂等。批任务不是简单把 SQL 搬到 Java,而是要设计任务状态、重试策略、分片边界和异常补偿。比如日报任务可以先删后插,也可以写入临时表后 rename 或按批次号切换;资金、佣金、保单状态类任务则不能随意删除重算,必须有流水和补偿机制。
5.3 SQL 扫描和分级
可以写一个轻量扫描工具,对 Mapper XML 和 SQL 文件做关键词识别,输出风险清单。例如扫描:
ROWNUM
CONNECT BY
START WITH
NVL(
DECODE(
TO_DATE(
TO_CHAR(
TRUNC(
SYSDATE
NEXTVAL
DUAL
||
扫描结果按模块、文件、Mapper 方法、风险类型、建议改法输出。这个工具不一定要特别复杂,但能把“靠人记忆”变成“清单驱动”。在项目管理上也能形成进度看板:总 SQL 数、已改造数、待验证数、高风险 SQL 数、慢 SQL 数。
6. 数据一致性验证
数据库迁移最怕“能跑但结果不一样”。因此一致性验证要分层做。
第一层是对象级验证。检查表数量、字段类型、索引、主键、唯一约束、默认值、非空约束、序列、视图等是否一致。字段类型尤其关键,比如 Oracle 的 NUMBER 迁移成 OceanBase 后要确认精度;日期字段要确认是否包含时分秒;大字段要确认字符集和长度。
第二层是数据级验证。常见方法是行数对比、主键范围对比、按批次 hash 对比、关键字段聚合对比。例如按天统计保单表:
SELECT DATE(create_time), COUNT(*), SUM(premium)
FROM policy_order
GROUP BY DATE(create_time);
这种聚合对比能快速发现漏数、重复、金额精度变化等问题。
第三层是 SQL 结果级验证。这个项目中可以设计“新旧 SQL 对比工具”:同一组输入参数,在 Oracle 和 OceanBase 分别执行 SQL,然后把结果集标准化后比较。
标准化要处理这些细节:
- 字段顺序统一。
- 日期格式统一到毫秒或秒。
NULL与空字符串按字段规则处理。- BigDecimal 去掉无意义尾零后比较。
- 结果集排序固定,避免数据库默认返回顺序不同。
- 分页 SQL 要分别验证第一页、中间页、最后一页和空页。
工具输出不应只有“成功/失败”,而应给出差异报告:缺失行、多余行、字段值不同、总行数不同、排序不同、耗时差异。这样开发和 DBA 才能快速定位是 SQL 语义问题、数据同步问题,还是排序不稳定。
第四层是业务流程验证。比如从创建客户、分配销售、生成商机、录入保单、统计业绩到日报展示,完整跑一条链路。迁移项目里,只有 SQL 对比是不够的,因为很多问题出现在事务边界、缓存、定时任务和上下游接口之间。
7. 性能优化
OceanBase 慢 SQL 优化要结合执行计划、租户资源和 SQL 写法来看。迁移后慢 SQL 变多,不一定说明 OceanBase 性能差,很多时候是 Oracle 时代积累的 SQL 写法在新执行器下暴露出来。
7.1 执行计划分析
优化时先拿到慢 SQL、绑定参数、执行耗时、扫描行数、返回行数和执行计划。重点看:
- 是否走了预期索引。
- 是否出现全表扫描。
- join 顺序是否合理。
- 过滤条件是否能下推。
- 排序、分组是否使用临时表。
- 返回行数很少但扫描行数很大,说明索引或条件有问题。
对于高频列表 SQL,要优先保证 where 条件和 order by 能匹配索引。例如:
WHERE agent_id = ?
AND status = ?
AND create_time >= ?
AND create_time < ?
ORDER BY create_time DESC
LIMIT 20;
可以考虑组合索引:
(agent_id, status, create_time)
如果排序需要稳定,再结合主键:
(agent_id, status, create_time, id)
7.2 函数导致索引失效
迁移中大量慢 SQL 来自字段上套函数:
WHERE DATE(create_time) = :bizDate
WHERE IFNULL(status, '0') = '1'
WHERE SUBSTRING(cert_no, 1, 6) = :areaCode
这类写法会削弱索引使用。优化方向是把函数从字段侧移到参数侧,或者增加冗余字段。比如日期查询改成范围,证件地区码可以在写入时生成 cert_area_code 字段并建索引。
7.3 大报表拆分
保险销售系统常见报表会关联客户、保单、产品、机构、人员、活动、佣金等多张表。迁移后如果一条 SQL 又长又慢,不一定要执着于把它调到极致。可以考虑:
- 把实时查询改成 T+1 汇总表。
- 把复杂维度预聚合到宽表。
- 把多次重复计算的组织树结果缓存或落表。
- 把大范围导出改成异步任务,生成文件后下载。
- 对报表查询增加时间范围、机构范围等强制条件。
优化不是只看单条 SQL,而是看业务是否真的需要实时、全量、明细级查询。面试时可以强调:我会先判断查询是在线交易链路还是离线分析链路,在线链路优先降低响应时间和资源消耗,报表链路则可以通过预计算、异步化和结果缓存来换稳定性。
8. 常见坑
第一,分页结果不稳定。没有明确 ORDER BY 的分页,本来就没有稳定顺序。迁移后数据库返回顺序变化,业务会误以为数据丢失。所有分页必须有确定排序,最好带唯一键兜底。
第二,COUNT(*) 和列表 SQL 语义不一致。分页插件自动生成 count SQL 时,如果原 SQL 有 GROUP BY、DISTINCT、UNION,简单包一层或去掉排序可能导致总数错误。
第三,空字符串和 NULL 混用。这个问题最隐蔽,经常表现为“某些客户查不到”“某个筛选条件变多或变少”。
第四,日期边界错误。把 TRUNC 改成日期范围时,要注意时区、闭开区间和毫秒精度。推荐使用 >= start 且 < nextStart。
第五,递归 CTE 没有限制深度。组织数据一旦有环,查询可能耗时异常。要加最大层级、路径去重或数据治理。
第六,存储过程 Java 化后事务变大。原来过程内部可能分批提交,改成 Java 后如果一个方法包住全部数据,会造成长事务、锁等待和回滚成本高。要按业务安全边界分批提交。
第七,只做行数校验。行数一致不代表金额、状态、时间、排序一致。核心表必须做关键字段 hash 或业务口径校验。
第八,忽视 OceanBase 租户资源。慢 SQL 不一定全是 SQL 问题,也可能是租户 CPU、内存、并发、连接数、统计信息不准或合并期间资源波动导致。
第九,上线后没有慢 SQL 基线。迁移前应该记录 Oracle 侧核心 SQL 耗时,迁移后才能判断是变快、变慢还是业务流量变化导致。
第十,回退预案不可执行。真正的回退不仅是“切回 Oracle”,还要考虑切换期间 OceanBase 新增数据如何处理、序列是否前进、外部接口是否产生副作用。
9. 面试追问
- 你们为什么从 Oracle 迁移到 OceanBase?是政策要求还是性能原因?
- 迁移前你们如何评估 SQL 兼容性?
ROWNUM和LIMIT的语义差异是什么?CONNECT BY怎么改成递归 CTE?如何防止环?- Oracle 空字符串和 OceanBase 空字符串有什么差异?
- 常见 Oracle 函数怎么迁移?哪些函数最容易影响索引?
- MyBatis 分页拦截器怎么实现?拦截哪个点?
- 分页插件自动生成 count SQL 有什么风险?
- 为什么要把存储过程改成 Java + XXL-Job?
- 存储过程 Java 化后如何保证幂等和重试安全?
- OGG 或数据库同步链路迁移时如何保证不丢数据?
- 新旧 SQL 对比工具怎么设计?
- 结果对比时如何处理日期、金额、空字符串和排序?
- 迁移后出现慢 SQL,你会怎么排查?
- OceanBase 执行计划重点看哪些信息?
- 深分页怎么优化?
- 报表 SQL 太慢,除了加索引还有什么方案?
- 上线切换怎么做灰度和回退?
- 迁移项目中你踩过最大的坑是什么?
- 如果让你重新做一次,你会提前建设哪些工具?
10. 推荐回答
如果被问“Oracle 到 OceanBase 迁移你主要做了什么”,可以这样回答:
我参与的是核心业务系统的信创数据库迁移,原系统长期运行在 Oracle 上,应用侧主要是 Spring 和 MyBatis,数据库里沉淀了比较多 Oracle 方言 SQL、存储过程、Job 和报表逻辑。我的工作不是单纯改连接串,而是负责应用侧兼容性改造和迁移验证,包括 SQL 风险扫描、ROWNUM 分页改造、CONNECT BY 树查询改递归 CTE、Oracle 函数差异处理、空字符串语义治理、MyBatis 分页拦截器适配,以及部分存储过程 Java 化后接入 XXL-Job。
具体做法上,我们先做资产盘点,把 Mapper XML、存储过程、视图、定时任务和高频报表 SQL 拉成清单,然后按风险分级。普通 CRUD 批量适配,分页和函数类 SQL 通过规则和人工复核结合处理,复杂树查询和报表 SQL 单独改造。对于原来在数据库里的批处理逻辑,我们评估后把一部分迁移到 Java Service,通过 XXL-Job 统一调度,并补了任务幂等、失败重试和执行日志。
为了降低上线风险,我还参与设计了新旧 SQL 对比工具。工具会用同一组参数分别连接 Oracle 和 OceanBase 执行 SQL,把结果集做标准化处理,比如日期格式、BigDecimal 精度、空字符串和 NULL、排序规则,然后输出差异报告。这样可以提前发现语义差异,而不是等业务测试靠肉眼发现。
上线后我们重点看慢 SQL 和核心链路耗时。迁移后有些 SQL 因为函数套字段、深分页、排序没走索引或者统计信息不准变慢,我们会结合执行计划、扫描行数、返回行数和索引设计来优化。对于报表类 SQL,不是只靠加索引,而是结合预聚合、汇总表、异步导出和查询条件收敛来处理。
如果要总结经验,我觉得数据库迁移的关键不是“语法替换”,而是保证业务语义一致、性能可接受、问题可回溯。工具化清单、双库结果对比、灰度切换和慢 SQL 基线非常重要。
如果被问“ROWNUM 到 LIMIT 有什么坑”,可以回答:
ROWNUM 和 LIMIT 最大的差异是执行语义。Oracle 的 ROWNUM 是返回行时生成的,所以如果分页 SQL 没有把排序放在内层子查询里,就可能先截断再排序,结果本身就不稳定。迁移时不能简单正则替换,要先确认原 SQL 的排序和分页语义。改成 LIMIT offset, size 后,还要关注深分页性能,对于大数据量列表,我们会改成基于最后一条记录的游标分页,并保证 ORDER BY 带唯一键,避免翻页重复或遗漏。
如果被问“CONNECT BY 怎么迁移”,可以回答:
Oracle 的 CONNECT BY 常用于组织树、团队树、客户归属树。迁移到 OceanBase MySQL 模式后,我们用递归 CTE 改写。锚点查询对应 START WITH,递归部分通过父子字段 join,对应 CONNECT BY PRIOR。改造时要特别注意递归方向、层级字段、兄弟节点排序和环形数据。生产上我会加最大层级或路径去重,同时对组织基础数据做治理,避免脏数据导致递归异常。
如果被问“为什么把存储过程改 Java + XXL-Job”,可以回答:
主要是为了可维护性、可观测性和调度治理。原来存储过程放在数据库里,版本管理、代码评审、日志、失败重试和告警都不如应用侧方便。迁移时我们把适合应用化的批处理逻辑改成 Java Service,再由 XXL-Job 调度,这样任务执行记录、失败重试、手工补偿、分片处理都更清晰。不过不是所有存储过程都应该机械迁移,强数据处理、短事务、数据库内部聚合逻辑也可以保留或分阶段改造。关键是按业务风险分层。
如果被问“迁移后慢 SQL 怎么排查”,可以回答:
我会先确认慢 SQL 是单次慢还是整体慢,是 SQL 本身问题还是租户资源问题。然后拿 SQL 文本、绑定参数、执行计划、扫描行数、返回行数、索引命中情况和等待事件。常见问题包括字段上套函数导致索引失效、分页 offset 太大、join 顺序不合理、统计信息不准、排序分组使用临时空间。优化时先保证核心过滤条件和排序匹配索引,再考虑 SQL 改写、预聚合、异步化或报表汇总表。对于高频交易链路,会建立迁移前后的耗时基线,避免上线后只凭感觉判断。
11. 延伸学习路线
第一阶段,补数据库迁移基础。重点学习 Oracle 常见对象、OceanBase MySQL 模式和 Oracle 模式差异、数据类型映射、索引结构、事务隔离级别、执行计划基础。目标是能看懂迁移评估报告。
第二阶段,补 SQL 兼容性。重点练习 ROWNUM、CONNECT BY、NVL、DECODE、日期函数、序列、字符串拼接、空字符串、分页 count、动态 SQL。建议把每一种差异都整理成“原 Oracle 写法、目标写法、风险、验证方式”。
第三阶段,补 MyBatis 插件机制。理解 Executor、StatementHandler、ParameterHandler、ResultSetHandler 的职责,掌握分页拦截器、SQL 改写、BoundSql 参数处理和插件链顺序。这样面试问到分页改造时,能从框架原理讲到项目落地。
第四阶段,补批处理和调度治理。学习 XXL-Job 的任务注册、路由策略、分片广播、失败重试、阻塞处理和日志机制。结合存储过程 Java 化,重点掌握幂等设计、任务流水表、补偿任务和分批提交。
第五阶段,补数据校验工具。可以自己实现一个小工具:读取 YAML 中的新旧数据源配置和 SQL 用例,分别执行后生成 JSON 差异报告。重点处理字段标准化、排序、金额精度、日期格式、NULL 和空字符串。
第六阶段,补 OceanBase 性能优化。学习租户资源、SQL 审计、执行计划、统计信息、索引选择、分区表、慢 SQL 诊断。最好拿真实慢 SQL 做练习:先解释为什么慢,再给出索引、SQL 改写或业务改造方案。
第七阶段,形成面试复盘材料。把项目整理成三张图:迁移总体流程图、SQL 兼容性分类图、上线验证和回退流程图。面试时先讲全局,再讲一两个代表性问题,比如 CONNECT BY 改 CTE、新旧 SQL 对比工具、慢 SQL 优化。这样既有高度,也有细节。