接手YashanDB之后,我踩过最疼的坑不是SQL写错,而是用Oracle那套默认直觉去操作它。YashanDB在语法兼容上做得相当细,内置函数、PL/SQL写法、系统包都有很高的重合度,可"兼容"和"同一套运行逻辑"完全是两回事。尤其是在行数上来、并发起来之后,很多问题根本不在于YashanDB本身,而是迁移、建模和运维习惯没有跟上。
这篇文章把我自己在真实项目里沉淀下来的5个工程化最佳实践整理出来:迁移接入怎么摸底、表结构怎么重建、事务并发怎么防锁、高可用怎么验证、性能问题怎么定位。每个方向我都会讲原理、讲操作、讲我实际撞过的坑,适合准备引入YashanDB的架构师、刚接手这个库的DBA,以及要面向它写业务SQL的研发同学。
1. 迁移接入:先摸清Oracle兼容边界,再谈"充分利用"
YashanDB最容易让人产生错觉的地方,就是第一眼看上去"什么东西都有"。DECODE能用、ROWNUM能用、存储过程语法也眼熟,于是团队很容易直接进入搬运阶段。我的建议是反过来,先花两个星期做兼容性摸底,再动真迁移,省下的时间远大于摸底的成本。
1.1 兼容性摸底:从一张自检清单开始
别信任何"99%兼容"的口头承诺,一切以你当前版本的实机验证为准。我一般会让团队按下面这张清单逐项过一遍测试库:
| 检查项 | 具体验证点 | 我实际踩过的坑 |
|---|---|---|
| 数据类型 | NUMBER(p,s)、VARCHAR2、DATE/TIMESTAMP、CLOB、BLOB | NUMBER(38,0)对账字段在高精度运算时出现意外的四舍五入 |
| 单行函数 | DECODE、NVL、NVL2、TO_CHAR格式串、TRUNC、正则函数 | TO_CHAR日期格式串部分写法与Oracle不完全一致 |
| SQL语法 | 层次查询CONNECT BY、外连接(+)写法、FETCH FIRST、MERGE INTO | 老代码里的(+)写法部分重写成ANSI JOIN后才稳定 |
| PL/SQL | 包、存储过程、自治事务PRAGMA、动态SQL、游标变量 | 包内重载、PIPELINED函数需要逐条跑边界用例 |
| 对象类型 | 序列、同义词、物化视图、DBLink、触发器 | 同义词链路多级嵌套时授权模型要重新梳理 |
| 系统包 | DBMS_OUTPUT、UTL_FILE等常见包 | 部分系统包参数行为有差异,不能只看"有没有" |
这张表的重点不是走马观花,而是让每一个"看起来能用"的功能都有一条真实的验收SQL。比如所有日期格式串、所有正则表达式函数,都写一个输入输出对照用例。我在项目里就用这种方式在三天内捞出四十多个差异点,其中一半以上是"换个写法就能绕过"的小问题,真正伤筋动骨的就三个——但这三个如果不提前发现,上线后每一个都是事故。
1.2 迁移链路:全量、增量、切换窗口一次想清楚
迁移方案不要做成"导出导入一把梭"。我比较习惯三段式。
第一段做全量数据迁移。这时候主要关注并行度和一致性。大表不要一个连接从头导到尾,拆成分区或者主键范围段,每个段一个并行任务,效率能提升好几倍。字段类型转换统一由迁移脚本管理,不要依赖导出工具自动转换,不然NUMBER到DECIMAL、DATE到TIMESTAMP这类映射很容易失控。
第二段做增量同步。YashanDB现在主流的迁移路径是通过其自身的CDC能力或者兼容协议的日志解析来做增量捕获。这里最容易忽视的是字符集和时区。源库和目标库的字符集如果不同,中文字段在高并发写入时可能出现长度计算偏差;时区配置不一致,TIMESTAMP字段切过去之后可能整体偏几小时,这种问题在测试阶段几乎不会被发现,上了生产才暴露。
第三段是切换窗口。我的原则是:切换不是一个动作,是一套回放流程。先用工具把增量追到一致点,然后停写、校验主键和记录数、再开启目标库应用。校验环节里除了行数,一定要对比几个关键业务表的MAX(主键)、几个金额字段的SUM、以及序列的当前值。序列是非常容易漏掉的一环,源库序列已经走到十万,目标库如果从一千起步,新数据马上撞主键。
1.3 应用侧SQL审计:老代码库需要一次工程化清点
真正耗时间的往往不是表和数据,而是应用里那几千条SQL。你不可能把一个跑了多年的Oracle库搬过来,还指望每个开发人都记得自己写过哪些Oracle特有用法。
这时候就需要在代码库层面做一次清点。大代码库靠人工翻不现实,我建议用代码扫描脚本把所有Mapper文件、XML、存储过程里的SQL全部捞出来做静态检查。现在不少团队已经在用AI辅助做这种审计,像claude code这类工具在处理大型代码库时上下文能力很强,让它带着"找出Oracle专有语法并给出YashanDB改写建议"的任务去扫一遍,产出效率比我当年人肉看高一个数量级。注意AI给出来的改写建议一定要在测试库执行验证,不能直接照搬。
高频替换项我列几个:ROWNUM翻页改成FETCH FIRST或者配合排序子查询;CONNECT BY先确认目标库的等价写法,如果遇到性能问题改成递归CTE加明确断开条件;外连接(+)写法统一改标准ANSI JOIN。驱动和连接串也要一起换,连接池里的最大连接数、连接超时时间这些参数要提前和DBA对齐,不然应用一上线就报连接不足,第一反应还以为是数据库的问题。
2. 表结构与索引:把"搬运"变成"重做"
很多人把数据迁移理解成把表结构和数据抄过去,这其实是错的。YashanDB的优化器和Oracle有亲缘关系,但不同版本、不同内核实现下的代价模型并不一样。如果一个表在原库靠某个索引跑得好,直接搬过来通常问题不大;但如果你把原库十多个冗余索引也一并搬来,写放大和存储浪费就要自己扛了。
2.1 分区策略:为查询和维护设计,不为"别人都有"设计
分区表不是建了就完事,YashanDB的分区裁剪机制决定了只有WHERE条件里带了分区键,优化器才会把它裁剪成只扫对应分区。我的一个经验是:分区键必须出现在业务查询的过滤条件里,否则分区表除了增加DDL复杂度,执行效率甚至可能更差。
| 分区类型 | 适用场景 | 典型业务字段 |
|---|---|---|
| 范围分区 | 流水、日志、订单,按时间滚动查询和归档 | 创建时间、日期字段 |
| 列表分区 | 区域、状态、机构等有限枚举值 | 省份、订单状态 |
| 哈希分区 | 单值查询多、数据分布均匀、无明显范围特征 | 用户ID、订单号散列 |
如果一张表既有时间范围清理需求,又有高频的租户维度查询,可以考虑两级分区或者选择时间分区加租户列全局二级索引。二级索引在YashanDB上对分区表非常重要,跨分区查询时能不能走索引,往往就决定接口是毫秒级还是秒级。
2.2 索引设计:先看查询模式,再决定要几个索引
我做索引设计时会给每张表列三样东西:核心查询的WHERE、排序字段、回表字段。然后按这个逻辑决定索引列顺序:等值条件的列放最前面,范围条件的列放中间,排序字段放在最后利用索引有序性避免额外排序。
实际项目中我总结了一个索引设计检查表,新索引必须逐条确认:
- 索引列里有没有被函数包裹(比如对create_time做TO_CHAR),有的话尽量改写SQL而不是建函数索引
- 查询有没有SELECT *,大字段回表会很痛,改成只查必要列
- 有没有条件列是"低选择性"却建了单列索引(比如性别、状态),这种索引可能被优化器忽略
- 组合索引的列顺序是不是把选择性最高、最常作为等值条件的列放前面
YashanDB的索引结构对这类问题给出的执行计划通常很直观:该用INDEX RANGE SCAN却走成了TABLE ACCESS FULL,就说明索引没吃上。这时候与其加索引,不如先看看SQL写法让不让索引生效。
2.3 统计信息:执行计划跑偏的根因,通常不是数据库笨
我在YashanDB上遇到的大部分"SQL突然变慢",最终都指向两个方向:一是统计信息陈旧,二是SQL写法不稳定。很多团队在生产环境做完数据迁移之后,没有第一时间重新收集统计信息,优化器拿着老库的老统计去估算新数据的大小,出现表实际五千万行、优化器以为只有五十万行的谜之自信,然后就全表扫。
我给运维团队定的规矩是:批量导入之后必须手动收集统计信息;每天有自动收集窗口兜底;大表千万行以上变更超过10%,不要等自动任务,手动跑一次。YashanDB对Oracle兼容生态做了很多适配,常用统计信息收集方式通常可以沿用,具体命令以你部署的版本文档为准。
倾斜数据是另一个容易踩的点。比如订单表里状态字段90%是"已完成",剩下10%分布在多个罕见状态。如果没有收集直方图,优化器默认所有值均匀分布,按"已完成"查询时可能算出返回行数很少,选了一个索引计划;而实际返回百万行,索引回表比全表扫还慢。处理办法是显式保证统计收集中包含直方图信息,并对高频值分布做抽查。
3. 事务与并发:在业务侧把锁冲突和死锁扼杀掉
YashanDB继承了一套比较成熟的MVCC并发控制模型,读不阻塞写、写不阻塞读这条底层能力是有的。但有一种情况永远绕不开:两个事务同时改同一行,写锁还是互斥的。所以并发问题真正的主战场不在"数据库能不能跑",而在"业务事务是不是被设计成了锁冲突体质"。
3.1 隔离级别:默认读已提交,别盲目升可串行化
数据库隔离级别不是越高越好。可串行化隔离级别能够避免幻读和其他异常,但代价是并发度大幅下降,锁的持有时间变长,冲突概率成倍上升。
我见过一个团队为了"保险"把所有事务都配成可串行化,结果一到业务高峰期,应用日志里全是锁等待超时。降回读已提交之后,绝大多数业务一条SQL不会多读脏数据,性能立刻恢复。需要真正做一致性快照读的场景,用只读事务或者显式事务去控制,而不是全局拉高隔离级别。
事务隔离级别选型建议:
- 默认使用读已提交,这是OLTP最合适的平衡点
- 需要多次查询结果保持一致时,用显式只读事务替代
- 可串行化只用于对账、跑批等并发度要求不高、正确性要求极高的场景
3.2 锁冲突与死锁:先看应用日志,再反推业务写法
YashanDB的锁等待一般会有一个超时控制,超过阈值后事务会报错回滚。应用日志里出现锁等待超时,很多人第一反应是调超时参数或者连数据库,我的建议是反过来,先去查这几个关键点:哪个事务持锁时间最长、它在等待什么、它的执行计划是不是慢到拖住了锁。
实际项目里最典型的一个死锁案例,是两张表先更新A再更新B的任务,和先更新B再更新A的任务同时跑。两边各自持有一把锁,又都在等对方手里的另一把锁,当场死锁给应用看。解决方案并不复杂:把所有任务统一成同一个加锁顺序,先更新主表再更新子表,死锁立刻消失。
给业务侧的几个硬规则:
- 涉及多张表的更新,事务内保持一致的加锁顺序
- 单事务更新的行数控制在一个合理范围,避免一次锁几千行
- 事务内不要做远程调用、不要等用户输入、不要放无意义的SLEEP
- 大批量更新拆成小批次,每批单独提交
3.3 事务边界:短事务是并发性能最好的朋友
长事务是锁冲突的温床。一个事务打开之后迟迟不提交,它锁过的每一行都会成为其他会话的阻塞点。实际生产中,我给研发同学定的标准是:一个OLTP事务里能放3条SQL,就不要塞10条;能50毫秒内提交完,就不要拖到1秒以上。
搬运Oracle业务时最容易进来的坏味道是循环里逐行更新。比如批量给一万个用户加积分,有人写成FOR循环里面一行一条UPDATE语句,每条语句一个事务,跑起来又慢又容易被其他会话打断。改成一次性UPDATE ... WHERE或者分批批量提交之后,总耗时降了一个量级,锁的影响范围也小了很多。批量提交时我一般按500到1000行一批,既降低单批事务的锁持有时间,又避免频繁提交导致日志落盘开销过大。
4. 高可用与备份:主备不等于备份,演练才算数
高可用这件事,最危险的不是没做,而是做了却从来没验证过。主备集群建好那天大家都觉得稳了,真正等生产故障发生才第一次做切换,那基本就等于默认接受"现场边查文档边操作"的结局。
4.1 高可用架构选型:先定RPO/RTO,再谈模式
高可用架构没有绝对好坏,只有适不适合你的SLA。先把应用能接受的数据丢失量(RPO)和恢复时间(RTO)定出来,再选形态。
| 架构模式 | 典型RPO | 典型RTO | 适用场景 |
|---|---|---|---|
| 单机+定期备份 | 取决于备份频率 | 小时级 | 开发测试、非核心系统 |
| 主备同步复制 | 接近零 | 分钟级 | 核心交易、要求不丢数据 |
| 主备异步复制 | 秒级到异步延迟 | 分钟级 | 多数OLTP系统 |
| 多副本一致性协议 | 接近零 | 秒级到分钟级 | 对RTO要求极高的场景 |
主备同步复制听着最安全,但它对网络抖动很敏感。两机房之间专线网络一旦延迟升高,主库的每次提交都要等备库确认,业务TPS会肉眼可见地下降。所以我不建议无脑上同步,而是先压测看网络质量,再决定用同步还是异步。
4.2 备份策略:全量加增量加日志归档,缺一不可
主备集群只能抗硬件故障,不能抗逻辑错误。一条误删数据的UPDATE在主备之间是会老老实实同步的,备库的作用是保证那台机器挂了还有另一台,而不是保证你手滑删掉的表能回来。真正常规的防护,是全量备份加上增量备份再加上日志归档的组合。
我的备份节奏是:核心库每天全量或者至少每周全量加每日增量,日志归档连续保留,能支持按时间点恢复。保留周期按业务需求来,一般核心数据保留30天以上。备份不是用来"心里踏实"的,是拿来恢复的,所以至少每个季度做一次完整的恢复演练。我项目里每次演练都会发现点"惊喜":备份文件名对不上、归档日志断档、恢复脚本路径写死了,都是小事,但真出故障时每一个都能救命。
4.3 切换与回切:应用连接池也要一起准备
主备切换的坑很大一部分不在数据库层,而在应用层。数据库已经切到备库了,应用连接池还是旧连接,应用不重连就一直报错。所以高可用方案里必须包含应用侧的配合:
- 连接池配置连接存活检测和自动重连,故障后能自动建立到新主库的连接
- 应用服务配置里不要写死单个节点地址,尽量走VIP或者连接代理
- 切换前提前通知应用团队,必要时滚动重启应用,让连接池干净起来
切换手册一定要写角色查询、备库延迟检查、切换命令、回切流程四段。每次切换前先确认备库延迟,延迟没追上就切,切换动作会扩大数据丢失的范围。我见过一次故障切换,切换过去之后才发现备库落后了十几分钟,从那之后我把"切换前检查备库延迟"写进了运维平台的开关里,延迟超过阈值直接拦截切换动作。
5. 性能调优:先定位再动手,别一慢就调参数
遇到"数据库慢",最常见的错误是先去调内存、调并发参数,然后发现没用,再从头排查。我的经验是性能问题九成以上出在SQL和执行计划层面,参数层面只占很小一部分。正确顺序永远是:定位慢在哪条SQL,看执行计划,分析为什么不用好索引,最后才是调参数。
5.1 慢SQL定位链路:应用日志到执行计划,一步都不能跳
我给团队定的排查链路是四条:
- 应用日志里找到报错或超时的SQL文本和参数
- 数据库慢查询日志里看这条SQL的耗时分布,确认是偶发还是常态
- 通过系统动态性能视图查该SQL的累计执行次数、总耗时、平均耗时、扫描行数和返回行数
- 拿到实际执行计划,看每一步的行数和耗时
这套链路里最关键的是"实际执行计划",不是Estimate计划。YashanDB兼容Oracle生态的特性让很多动态性能视图的观察习惯可以直接复用,但实际视图名和字段以你的版本文档为准。看计划时我一般先看有没有大表的全表扫描,再看连接顺序和连接方式,最后估算执行计划里的行数预估和实际行数偏差大不大。
5.2 执行计划分析:三个关键词看懂八成问题
执行计划里不需要懂每一个算子,只要盯住三个关键词就够解决大部分问题:全表扫描、索引范围扫描、连接方式。
全表扫描本身不是罪过。小表全表扫比走索引还快,这是正常的。真正要警惕的是几千万行的大表在核心查询里走全表扫描,尤其过滤条件明明能拿到索引却没用。这种时候先不要急着加索引,先看是不是SQL写法导致索引失效。
连接方式上,嵌套循环连接适合小驱动表大返回集场景,哈希连接适合大表等值关联。优化器选择哪种连接方式,依据是它对行数的估算。行数估错,连接顺序就会反。我处理过一个两张大表关联的慢查询,优化器用小表反查大表,结果基数估算偏差导致驱动表选反了,跑了二十秒。重新收集统计信息之后仍然不满意,最后把子查询改成带过滤条件的公共表表达式写法,SQL直接降到百毫秒级。所以说,调优很多时候是收集统计信息和改写SQL的组合拳。
5.3 参数调优的边界:先测基线,一次只动一个
终于说到参数了。YashanDB可调的参数不少,内存类、并发类、日志类各有一批。但参数调优最容易翻车的地方是"听说什么好就改什么"。网上那些"万能配置"大多来自某台特定硬件的特定负载,换一台机器可能完全不对。
我建议把参数调优当成一次受控实验:
- 调之前先在业务高峰期记录一组基线数据:CPU、内存、IO、活跃会话数、TOP SQL耗时
- 一次只改一个参数,改完观察至少三到五天
- 对比基线判断参数是正向作用还是副作用,没有明显收益就回滚
- 每次变更留记录,方便回溯
很多调优需求其实不是参数问题,而是容量问题。连接数降到阈值、活跃会话阻塞,与其调大连接数让数据库更忙,不如先看是不是慢SQL把连接都占满了。先解决"为什么慢",再决定"要不要加资源"。
我自己在实际项目里的体会是,YashanDB是一个值得认真对待的数据库,它把Oracle生态的门槛降得很低,但真正决定系统稳不稳的,还是迁移前有没有摸底、建表时有没有重做、事务有没有设计成短事务、备份有没有演练、慢SQL有没有一套排查链路。这五个方向做得越扎实,数据库本身反而没什么可操心的。最后随手分享一个小技巧:把第一节那张兼容性自检清单做成自动化回归脚本,以后每次版本升级先跑一遍,能帮你省掉大量"升级后才发现行为变了"的麻烦。