ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SQL迁移避坑指南:从对象盘点、方言改写到达梦实战

SQL迁移避坑指南:从对象盘点、方言改写到达梦实战 上周刚做完一个SQL迁移项目把一套老系统的数据库整体搬到了新的数据库平台。接手前我以为这活儿不复杂——SQL迁移嘛就是把SQL脚本拷过去跑一遍。真正做起来才发现这四个字的坑比想象中深得多。SQL迁移的核心从来不是迁SQL而是把分散在数据库里的表结构、数据、存储过程、权限、序列、应用侧的SQL语句整体搬到一个新的平台并保证行为一致。这篇文章不是讲解某个数据库新特性的而是把一次真实SQL迁移项目中反复踩到的点整理出来适合正在做数据库迁移、异构切换或者准备给新项目做数据库选型的同学参考。1. 迁移之前先盘点SQL迁移迁的到底有哪些东西很多人接到迁移任务的第一反应是建表、导数据。但真正专业的做法是先回答一个问题这个数据库里到底有多少种东西要搬我第一次做迁移时就漏了序列和定时任务结果上线第二天报表任务全部失效凌晨爬起来补脚本印象极其深刻。1.1 一张迁移清单帮你避免漏项我习惯在动手之前先把源库的对象逐个查出来形成一张清单。别小看这一步它能直接决定迁移范围是否完整。下面这张是我常用的分类表类别典型对象容易漏的原因结构对象表、视图、物化视图、序列、同义词视图依赖的对象顺序很容易搞错约束与索引主键、外键、唯一约束、CHECK约束、函数索引函数索引在不同数据库里语法差异很大程序对象存储过程、函数、触发器、包混合了动态SQL和事务控制改写工作量大权限对象用户、角色、对象授权、默认表空间很多工具默认不迁移权限运维对象定时任务、作业、调度、事件经常不属于业务部门管理没人提应用侧对象配置文件里的连接串、ORM映射、XML/注解里的SQL根本不在数据库里靠代码仓库检索实际盘点过程中应用侧SQL最容易漏。一个系统的SQL不只是存在数据库里还有大量散落在Java代码、MyBatis XML、存储过程调用语句、定时脚本、报表模块中。迁移前我建议把代码仓库全局搜一遍搜关键词如SELECT、INSERT、UPDATE、DELETE、CALL把结果按文件归类之后逐个核对是否能在新库跑通。这一步很枯燥但能提前暴露80%的兼容性问题。1.2 为什么存储过程、触发器、序列最难迁如果只迁表和视图SQL迁移的工作量至少能砍掉一半。麻烦往往出在程序对象上。存储过程难难在每家的流程控制写法不一样。MySQL里你习惯用DELIMITER和BEGIN...END包一段逻辑达梦和Oracle风格则更接近PL/SQL用AS开始过程体异常处理是EXCEPTION WHEN OTHERS THENSQL Server又不一样要用BEGIN TRY...BEGIN CATCH。更麻烦的是很多存量存储过程里混着动态SQL、隐式游标、临时表、事务嵌套这些在新环境里往往需要重新设计不是改两行语法就能编译通过的。触发器也是这样尤其是要注意触发时机和NEW/OLD字段的引用方式。MySQL里写NEW.column_nameSQL Server里写INSERTED.col_name达梦里两者都支持但语义不同改写时稍不留神就会引入幽灵数据。序列和自增列更是重灾区后面我会单独展开讲。我的经验是盘点阶段就把程序对象清单单独拉出来逐条标注工作量等级能在上线前预留足够的改写时间。如果项目周期很紧宁可先砍掉非核心触发器也不能让它们拖垮整体迁移计划。2. SQL方言差异对照表数据类型、函数、分页的改写三板斧盘点结束后真正进入SQL改写阶段。你很快会发现SQL标准在现实中是个理想标准每家数据库都有自己的方言。这里我把实操中最常遇到的差异分成三类数据类型、函数写法、分页与语法细节。只要把这三板斧解决掉日常业务SQL基本就能跑通了。2.1 数据类型映射最容易报类型不匹配的地方不同数据库对类型名和长度定义的理解不一样直接照搬DDL几乎必报错。下面这张表是我常用的映射关系以最常遇到的MySQL迁移场景为例MySQLOracle / 达梦PostgreSQL备注TINYINTNUMBER(3) 或 SMALLINTSMALLINT注意无符号unsigned时范围要大一级DATETIME / TIMESTAMPTIMESTAMPTIMESTAMPOracle的DATE不含毫秒要区分VARCHAR(n)VARCHAR2(n)VARCHAR(n)Oracle/达梦的VARCHAR2按字节数不是字符数TEXTCLOBTEXT大批量插入时注意性能BLOBBLOBBYTEAJDBC驱动处理方式不同ENUM / SETVARCHAR(n) CHECKVARCHAR(n) CHECK枚举约束需要手工补上JSONJSON / CLOBJSON / JSONB函数差异大建议先统一到字符串处理这里最容易踩的坑是VARCHAR的长度单位。MySQL里VARCHAR(20)表示20个字符但Oracle和达梦默认按字节算。如果业务里存中文VARCHAR2(20)实际只能存6到10个汉字。迁移时我习惯统一用VARCHAR2(n CHAR)或直接按4倍字节数放大宁可多占点空间也不要上线后才发现插入报长度超限错误。2.2 函数替换IFNULL、NOW、DATE_FORMAT们的归宿函数差异是SQL改写里最烦的部分因为没有统一的映射表可以照抄得结合业务实际来判断。下面是几个出现频率最高的替换组合场景MySQL写法通用/其他库写法空值替换IFNULL(col, 默认值)COALESCE(col, 默认值)Oracle用NVL当前时间NOW() / SYSDATE()CURRENT_TIMESTAMPOracle用SYSDATE日期格式化DATE_FORMAT(col, %Y-%m-%d)TO_CHAR(col, YYYY-MM-DD)取字符串长度CHAR_LENGTH(col)LENGTH(col)注意Oracle按字节数字符串拼接CONCAT(a, b) 或 a || b标准SQL推荐a || bSQL Server老版本用分组聚合拼接GROUP_CONCAT(col)LISTAGG / STRING_AGG注意去重和长度限制取子串SUBSTRING(col, 1, 10)SUBSTR / SUBSTRING参数起始位置含义要验证这里提醒一句窗口函数大部分已经标准化的说法其实只适用于较新的数据库版本。ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...)在主流数据库的新版本里都能跑但如果源库是MySQL 5.7或SQL Server 2008这类旧版本窗口函数通常需要改写为自连接加变量模拟工作量会明显增大。遇到这类情况我建议优先评估源库版本是否有升级计划如果目标库本身支持窗口函数那可以直接改写否则就得从业务逻辑层面重新实现。2.3 一条业务SQL从MySQL到达梦的改写全过程光说规则不够看一个实际例子更直观。假设源库有这条SQLSELECT id, name, IFNULL(remark, 无) AS remark, DATE_FORMAT(created_at, %Y-%m-%d) AS created_date FROM user_profile WHERE status 1 ORDER BY created_at DESC LIMIT 50;这条语句在MySQL里跑得很欢但直接扔到达梦里LIMIT语法、IFNULL函数、DATE_FORMAT函数都会报错。改写成达梦可用的版本是这样SELECT id, name, COALESCE(remark, 无) AS remark, TO_CHAR(created_at, YYYY-MM-DD) AS created_date FROM user_profile WHERE status 1 ORDER BY created_at DESC FETCH FIRST 50 ROWS ONLY;虽然达梦较高版本也兼容部分MySQL写法但我的建议是尽量改写成标准SQL或目标库原生写法不要依赖兼容模式。因为有兼容模式保底的写法往往性能不是最优而且容易出现本地能跑、测试环境正常、生产环境执行计划变慢的奇怪问题。3. 四种数据搬家策略怎么选停机、物理、逻辑、增量一个都不能少SQL改写只是半边天另一半是数据本身怎么搬。迁移方案的本质是在停机时间、数据一致性、实施复杂度三个维度之间做权衡。下面把四种常用策略一次性讲清楚。3.1 策略一停机全量迁移最简单但窗口要够最简单粗暴的方式业务先停掉应用停机维护然后用导出导入工具把数据全量搬到新库。这种方案适合数据量不大、能在几分钟到几小时内完成的系统比如内部管理后台、报表系统、离线分析库。实施步骤一般是先停应用在源库做一次快照或锁定写入导出结构再导出数据导入目标库最后做校验。优点是逻辑简单、一致性有保障缺点是停机时间完全取决于数据量大表一多就非常被动。我见过一个系统因为单表2亿行全量导出加导入跑了十几个小时远超预期最后只能临时改成线上同步方案。所以停机方案务必提前压测不要按导出工具显示的预估时间来算。3.2 策略二物理备份迁移同构之间最省事如果源库和目标库是同构的比如MySQL到MySQL、SQL Server到SQL Server物理备份迁移是最省事的方式。用备份文件直接还原或者直接拷贝数据文件速度远快于逻辑导出导入。以MySQL为例用逻辑导出mysqldump是逐条INSERT而物理备份工具如XtraBackup是在文件层面操作速度快一个量级。物理迁移的代价是同构限制。MySQL的物理文件不能直接还原到PostgreSQLSQL Server的备份也不能还原到Oracle。所以这个策略只适合同库升级、换机器、换集群这类场景。另外还要注意版本兼容有些旧库的物理文件在新版本里虽然能还原但会有升级提示索引统计信息也要重新收集。3.3 策略三逻辑导出与ETL工具异构迁移的主力异构数据库之间比如MySQL到达梦、Oracle到PostgreSQL、SQL Server到达梦物理迁移走不通只能走逻辑导出和ETL。具体做法可以是源库导出SQL/CSV再用工具转换导入也可以用现成的ETL工具做字段映射和分批同步。工具层面我比较常用的是DataX、Kettle这类它们都能做异构数据同步支持数据源插件扩展也支持按字段映射和自定义转换逻辑。对于数据中台建设中的异构系统整合场景这类工具几乎是标配。用它们的时候要注意两点一是尽量按主键或自增ID分批抽取不要一把梭SELECT * FROM大表否则内存直接打满二是目标端先建好表结构和索引别让工具一边建表一边导数。3.4 策略四增量同步解决不停机迁移的关键现在很多业务要求迁移期间应用不停机那就只能用增量同步方案。思路是先把全量数据同步过去再用CDC工具捕获源库的增量变更持续写入目标库等两边数据追平后在一个极短的切换窗口内完成应用切换最后停掉同步任务。MySQL场景下常用的方案是解析binlog比如Canal、Debezium、Flink CDCPostgreSQL可以用逻辑复制SQL Server有CDC功能。这里我特别想提醒一个问题增量同步工具对DDL的处理普遍不完整。ALTER TABLE加个字段、改个索引很多工具会直接报错或跳过导致目标库结构漂移。所以迁移期间要约定源库禁止DDL变更或者所有DDL走审批并人工在目标库同步执行。还有一个更隐蔽的坑是binlog必须设置成ROW格式否则增量解析会拿到错误的变更数据。这个可以在源库参数里提前确认。4. 以MySQL到达梦为例一场典型的异构SQL迁移全过程如果说前几节是通用理论这一节就用一个具体场景把它们串起来把一套MySQL 5.7业务库迁移到达梦DM8。为什么选这个例子因为这是我在实际项目里接到最多的需求类型尤其是在数据库选型升级和异构系统整合的背景下MySQL到达梦几乎成了标准考题。4.1 迁移前要做的三件准备第一件确认达梦实例的初始化参数。达梦8在初始化实例时可以选兼容模式常见有Oracle兼容和MySQL兼容。如果你的SQL以MySQL方言为主建议初始化时设置MySQL兼容模式如果历史包袱少也可以选Oracle兼容模式因为达梦在Oracle语法上的成熟度通常更高。这里要谨慎实例初始化后兼容模式很难改选错后续所有SQL改写都会很痛。第二件确认字符集和大小写敏感规则。MySQL里表名默认不区分大小写而达梦默认区分大小写。如果应用代码里对表名的使用大小写不一致迁移后可能直接报无效的表名。另外字符集要统一源库是utf8mb4目标库一般也设置成UTF-8JDBC连接串里加上characterEncoding参数避免写入中文变成问号。第三件准备好迁移工具。达梦的官方迁移工具一般叫DTSDM数据迁移工具也有图形化界面能连接MySQL、Oracle、SQL Server等多种源库。它做基础的表结构、数据迁移是够用的但它不是万能的后面会说哪些地方需要手工兜底。4.2 迁移工具的局限与手工兜底以DTS这类图形化迁移工具为例它的使用流程大致是新建工程配置源库连接MySQL JDBC配置目标库连接DM选择要迁移的对象执行迁移最后看迁移日志。表结构、数据这类常规对象工具能自动处理但以下几类常常需要手工介入自增列MySQL的AUTO_INCREMENT在到达梦后工具可能生成普通INT字段也可能生成IDENTITY列。要确认是否勾选了自增列映射为IDENTITY否则后续插入必须自己维护最大值非常容易冲突。ENUM和SET达梦不支持ENUM类型工具多半会转成VARCHAR并丢掉约束。需要手工补CHECK约束否则脏数据会在不知什么时候钻进来。触发器里的中文注释和特殊字符工具迁移时容易出现乱码一般建议触发器手工迁移不要依赖工具。视图依赖顺序如果视图之间有依赖工具按名称顺序迁移会直接报错建议先导出视图定义按依赖关系排序后手工执行。4.3 一个典型卡点ON UPDATE CURRENT_TIMESTAMP的替代方案MySQL里给字段加了ON UPDATE CURRENT_TIMESTAMP后每次行更新都会自动刷新时间戳这个写法业务上很常用但达梦默认不支持这个语法。迁移时需要在目标库用一个触发器来模拟CREATE OR REPLACE TRIGGER trg_user_profile_upd BEFORE UPDATE ON user_profile FOR EACH ROW BEGIN :NEW.updated_at : CURRENT_TIMESTAMP; END;类似的隐性功能还有不少比如MySQL的GROUP_CONCAT默认长度限制、INSERT IGNORE语义、REPLACE INTO语义这些都要在迁移前逐条确认。我的习惯是拉一张源库特性清单把业务里实际用到的MySQL独有特性列出来逐个找目标库的替代写法比迁移时边报错边查资料高效得多。5. 验货不能只数行数数量、结构、语义三层校验缺一不可数据搬过去了SQL也改完了能不能说迁移完成我的答案是不能。上线前必须做一套完整的校验否则你只是把错误从A环境搬到了B环境。我通常分三层来验先验数量再验结构最后验业务语义。这三层每一层都能抓出不同类型的问题。5.1 第一层行数与表结构核对最基础也是最容易偷懒的一步核对每张表的行数。但直接用COUNT(*)全表统计在亿级大表上会把人等疯。我建议用分片并行统计的思路先找出表中的主键或唯一索引按主键范围切成N个片段并行执行COUNT最后汇总。-- 以id主键为例分片统计示例 SELECT COUNT(*) FROM big_table WHERE id 1000000; SELECT COUNT(*) FROM big_table WHERE id 1000000 AND id 2000000; -- 汇总后与源库导出时的total值比对除了行数结构核对也很关键。可以写一条跨库的元数据查询把每个表的字段名、类型、长度、默认值、是否为空导出来做一次全量对比。这里最容易发现的是默认值丢失、字符集不一致、字段长度被工具自动缩小等问题这些单靠跑业务用例很难暴露但会在某天某个深夜突然冒出来。5.2 第二层数据样本与校验和比对行数一样不代表数据内容一样。更严格的做法是校验和比对思路是对每一张表选择几个关键列计算一个聚合校验值。比如对数值列做SUM对文本列做MD5拼接对日期列做MAX和MIN然后比对源库和目标库。-- 以订单表为例源库和目标库分别执行后比对结果 SELECT COUNT(*) AS cnt, SUM(amount) AS sum_amount, MD5(MAX(remark)) AS max_remark_md5 FROM orders;这个思路还能延伸到抽样比对按主键抽样取1000行逐字段比对这样能在不做全表扫描的前提下拿到较高的数据一致性置信度。缺点是校验SQL需要在两套数据库上分别执行写法会因为函数差异略有不同但总体成本比全量逐行比对低得多效果却好得多。5.3 第三层业务SQL回归与性能摸底结构和数据都对上了业务还不一定对。第三步是把真实业务SQL拿到新库上跑一遍这就是业务语义校验。最有效的办法是从源库的慢日志、应用日志里收集最近一段时间真实执行过的SQL去除重复和参数化后在目标库上逐一执行看是否报错、是否能返回预期结果。这一步相当于给新库做一次体检。性能摸底也不能省略。迁移后一定要重新收集统计信息MySQL里是ANALYZE TABLE达梦和Oracle里是DBMS_STATS.GATHER_TABLE_STATS或类似调用。统计信息不准执行计划会乱选索引之前跑得不错的SQL可能突然变慢。我还会把高频SQL用EXPLAIN跑一遍重点看是否出现全表扫描、类型转换导致索引失效、表连接顺序变化。回滚方案也要提前写在发布计划里不要等出了问题再现场想。6. 踩坑记录字符集、自增列、约束顺序与大批量导入最后把这次迁移里真实踩过、也最值得记录的坑集中列出来。有些问题是迁移工具导致的有些是数据库差异导致的但它们的共同点是文档里不写只有实际动手才会碰到。6.1 字符集乱码不是玄学是链路里某个环节设错了迁移后最容易遇到的诡异问题就是中文乱码。排查下来问题常常不在导入数据这一步而在JDBC连接串。连接MySQL时没加characterEncodingutf8mb4连接达梦时没加字符集参数数据经过驱动就被翻译坏了。更隐蔽的是排序规则MySQL的utf8mb4_general_ci和达梦的排序规则不同同样的ORDER BY中文结果顺序可能不一样报表里看起来就是顺序不对。解决办法是迁移前先用一小张含中文的表做端到端验证确认整条链路没问题再放开全量。6.2 自增列与IDENTITY第一个插入就撞主键MySQL的AUTO_INCREMENT自增列迁移到达梦后如果被转成普通INT应用插入时必须自己查MAX(id)1并发一高就会撞主键。如果是IDENTITY列导入存量数据时又有另一个坑IDENTITY默认不允许显式指定值导入历史数据前要先把IDENTITY_INSERT特性打开导完再关掉否则旧数据的ID直接丢失或错乱。我当时的做法是先把自增起始值重置为MAX(id)1再恢复应用写入这样新旧数据才不会冲突。这类问题在Oracle序列、PostgreSQL序列身上也会有类似表现序列步长、缓存、NOCACHE设置迁移前要把主键生成机制作为独立专项检查而不是把它当作普通字段处理。6.3 约束、索引与大批量导入的执行顺序最后一个常见坑是导入顺序。如果先建好所有外键再导入数据每次INSERT都会触发外键校验大文件导入会慢到让人怀疑人生。更麻烦的是一旦源数据本身有极少数孤儿记录导入会在中间突然报错中断把大量时间浪费在找问题数据上。我更推荐的做法是导入期间先禁用或暂缓外键约束数据导完后做一轮完整性校验最后再重建外键和索引。这不是让你跳过约束检查而是把检查的时机往后挪用一次统一的校验替代逐条导入校验。需要注意的是禁用外键后如果有应用并发写入可能会产生新的孤儿数据所以做这一步时必须保证业务是停机状态。大批量数据导入还有一个技巧按主键分片每500到1000条提交一次避免单个大事务把undo撑爆。TEXT、BLOB这类大字段多的表分片批次还要更小一点否则一旦失败回滚时间会特别长。SQL迁移做下来我的整体体会是它真正考验的不是SQL语法本身而是做事的顺序和检查的习惯。先盘点、再改写、分批搬、反复验这套流程本身就是最大的护身符。如果你马上也要做类似的数据库切换我建议你把文中的迁移清单打印出来一项项打勾遇到拿不准的SQL就先在当前库验证不要留到上线那天。下次有人再跟你说就一个SQL迁移嘛你可以把这篇文章甩给他让他先看看这里面的项目再拍胸口。
返回列表