
最近刚协助团队完成了一个从MySQL到人大金仓数据库的迁移项目迁移过程中SQL语法差异是第一个绕不开的坎。你可能觉得不都是SQL吗能有多大差别真上手之后才发现一个分页写法、一个空值判断函数就让应用报错半天。这篇文章是这次迁移踩坑的记录也是目前我见过最完整的MySQL与人大金仓SQL语法差异整理适合正在做数据库国产化改造的开发、DBA、运维同学直接对照参考。在开始之前先交代背景。我们手里的系统是Java MySQL 8.0SQL数量接近2000条包含大量存储过程、触发器、复杂的统计报表SQL。迁移目标是大金仓KingbaseES团队里没有任何人是金仓的使用经验。整个迁移预算时间就一个月一边要保证线上业务不停一边要完成技术栈切换。前一周几乎都在跟语法差异较劲后面才逐渐摸到规律。这篇文章就把这些规律和具体差异点全部列出来每个差异点都附上代码对照你可以直接把它当迁移手册用。1. 为什么要做这份SQL差异对比1.1 一个真实的迁移场景我们接到任务时首先面对的是存量系统改造。也就是说系统已经用MySQL稳定运行了好几年现在底层的数据库要换成人大会仓应用代码不能大动最理想的情况是把数据库驱动换掉、连接串改掉业务逻辑层尽量保持不动。但现实是任何一个用到了MySQL方言的SQL到了金仓上都有可能变成一条语法错误。举个最简单的例子应用里非常常见的分页查询-- MySQL 写法 SELECT * FROM t_order ORDER BY create_time DESC LIMIT 20, 10;这条语句在金仓里直接报语法错误金仓要求LIMIT和OFFSET的顺序反过来-- KingbaseES 写法 SELECT * FROM t_order ORDER BY create_time DESC LIMIT 10 OFFSET 20;类似这种“看着一样、用着不一样”的地方加起来有一箩筐。如果项目里恰好是用了MyBatis并手写大量SQL那这里的每一项差异都可能变成生产环境的一个BUG。那有没有可能直接用ORM框架自动适配分情况。如果用的是Spring Data JPA、Hibernate这类高度封装的框架方言已经帮你处理了大部分差异如果用的是MyBatis这种半自动框架SQL写死了MySQL语法那无论如何都要改一遍。我们项目就是后者所以才有必要把差异点整理成一份文档分发给所有开发统一按规范改。1.2 理解金仓内核有助于预判语法行为先说一个判断语法差异的底层技巧。人大金仓数据库KingbaseES的内核实际上衍生自PostgreSQL所以它在SQL行为上大量保持了PostgreSQL的习惯同时还提供了一套Oracle兼容模式。这意味着当你不确定某个写法在金仓上能不能用时可以先按PostgreSQL的语法去试大概率是可行的。比如金仓中常见的字符串聚合函数STRING_AGG就是PostgreSQL生态里的标准函数再比如正则表达式运算符~也是PostgreSQL的用法。这和MySQL的语法体系完全是两条路线MySQL更接近早期的SQL标准加自己的扩展金仓则是在PostgreSQL基础上做增强。这样理解之后我们做迁移时就有了一条判断原则遇到不确定的SQL先把它往PostgreSQL的标准写法上靠如果涉及存储过程、包、触发器这类比较复杂的对象再去看金仓的Oracle兼容模式。下文讲到的差异点绝大多数也遵循这个规律。2. 数据类型与建表语句的差异对比2.1 整数类型与自增列的处理MySQL里常见的小整数类型比较多比如TINYINT、MEDIUMINT、INT、BIGINT每个的存储范围不一样。但这几种类型在金仓里并不是都有对应项尤其是TINYINT和MEDIUMINT在金仓里根本没有这种类型。实际迁移时我们统一把TINYINT替换成了SMALLINT把MEDIUMINT替换成了INTEGER。这里有个隐藏的坑MySQL的TINYINT(1)经常被用来存储布尔值但金仓里有专门的原生BOOLEAN类型。从TINYINT(1)迁到BOOLEAN之后应用查询返回的结果从0/1变成了true/falseJava后端如果直接用Integer去接结果就会出问题。我们的处理方案是应用代码里凡是接收状态字段的地方都改成Boolean或者干脆不转BOOLEAN类型继续使用SMALLINT减少改动面。自增列是另一个大坑。MySQL里简单写id INT AUTO_INCREMENT就可以金仓里则有两种主流写法-- KingbaseES 写法一SERIAL CREATE TABLE t_user ( id SERIAL PRIMARY KEY, name VARCHAR(64) ); -- KingbaseES 写法二IDENTITY推荐 CREATE TABLE t_user ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(64) );SERIAL是PostgreSQL的老写法本质上会自动创建一个序列sequence列默认值取序列的下一个值。IDENTITY是SQL标准写法金仓支持得也很好。我建议新开发表都用IDENTITY因为语义更清晰。还有一个必须提醒的点迁移数据后序列的当前值和表里的主键最大值往往是对不上的。如果你用SERIAL的方式建表数据从MySQL导入后没有手工同步序列值接下来插入新数据就会报主键冲突。需要在导完数据后执行一次类似这样的操作SELECT setval(t_user_id_seq, (SELECT MAX(id) FROM t_user));这个问题在MySQL中不存在因为AUTO_INCREMENT会自动跟着当前最大值走。在金仓里序列是独立对象不会自动感知数据变化所以这也是迁移清单里必须核对的环节。2.2 字符串、日期与布尔类型差异字符串类型方面MySQL里的VARCHAR、CHAR、TEXT在金仓中都有对应。有一个小差异值得注意MySQL的TEXT类型在建表时不能设置默认值但金仓的TEXT是可以带默认值的。所以如果从MySQL迁移过来遇到了“TEXT字段有默认值”这种情况在MySQL那边是建不出来这样的表但在金仓里可以正常执行。日期时间类型是重灾区。MySQL常用DATETIME、TIMESTAMP、DATE金仓里则常见TIMESTAMP、TIME、DATE。MySQL的DATETIME没有时区概念而TIMESTAMP会随数据库时区转换金仓的TIMESTAMP也有WITH TIME ZONE和WITHOUT TIME ZONE的区别如果不指定默认是没有时区的TIMESTAMP。实际迁移中我们遇到一个比较典型的场景MySQL里的datetime字段存储的是本地时间到了金仓后如果谁不小心定义成了TIMESTAMP WITH TIME ZONE然后数据库会话时区又和业务时区不一致查出来的时间就会偏移几个小时。处理这类问题时建议统一用TIMESTAMP WITHOUT TIME ZONE应用层自己控制时区。日期函数这块差异更大。MySQL里把日期格式化成字符串喜欢用DATE_FORMAT到了金仓要改用TO_CHAR-- MySQL SELECT DATE_FORMAT(create_time, %Y-%m-%d %H:%i:%s) FROM t_order; -- KingbaseES SELECT TO_CHAR(create_time, YYYY-MM-DD HH24:MI:SS) FROM t_order;注意两个格式符体系完全不一样MySQL的%Y对应金仓的YYYYMySQL的%H:%i:%s对应金仓的HH24:MI:SS。手工转换的时候很容易漏掉建议全局搜索DATE_FORMAT、STR_TO_DATE、NOW等关键字逐个核对。布尔类型前面提到过金仓原生支持BOOLEAN可以直接存true/false。MySQL没有原生BOOL用TINYINT(1)代替。如果应用有返回布尔值的查询建议都要检查一下接收类型。2.3 一张建表语句的对照实例把上面的差异综合一下我直接做一个建表语句对照。左边是原MySQL表的写法右边是迁移到金仓后的等效写法-- MySQL 原始写法 CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键ID, name VARCHAR(64) NOT NULL COMMENT 姓名, age TINYINT DEFAULT 0, status TINYINT(1) DEFAULT 1 COMMENT 状态, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin COMMENT用户表;-- KingbaseES 调整后 CREATE TABLE t_user ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(64) NOT NULL, age SMALLINT DEFAULT 0, status SMALLINT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); COMMENT ON TABLE t_user IS 用户表; COMMENT ON COLUMN t_user.id IS 主键ID; COMMENT ON COLUMN t_user.name IS 姓名; COMMENT ON COLUMN t_user.status IS 状态;从这张表能看出几个关键改动去掉了ENGINE、CHARSET、COLLATE这些MySQL特有的存储引擎和字符集定义。表注释和列注释不再写在字段定义里而是拆成COMMENT ON TABLE和COMMENT ON COLUMN语句。TINYINT改成了SMALLINTTINYINT(1)也先保守地用SMALLINT而不是转成BOOLEAN。ON UPDATE CURRENT_TIMESTAMP这个MySQL的特性金仓不支持。如果确实需要更新时自动维护时间得用触发器实现。触发器的写法差异后面会专门讲这里先不展开。但建表阶段的取舍很重要它决定了后续数据迁进来之后应用能不能正常运行。3. 查询与修改语句的语法差异3.1 分页、排序与空值行为分页前面已经举例了MySQL的LIMIT offset, count和金仓的LIMIT count OFFSET offset是两个方向。这里多提一句很多持久层框架会把分页参数拼接进SQL如果你在MyBatis里自己写了limit #{offset}, #{pageSize}这种写法迁移后必须改掉。排序的空值行为也是一个容易踩的差异点。MySQL默认空值在升序排列时排在最前面金仓则相反空值默认排在最后面。假设你有一张订单表需要对退款时间refund_time做升序排列MySQL的结果是所有未退款订单NULL排在最上面换到金仓后所有已退款订单排上面NULL排最后。如果业务上没有明说这种顺序变化不会有人注意到但产品经理如果拿数据对比时发现顺序不对就会找过来排查。解决办法是显式指定空值位置-- 升序且NULL排前面 SELECT * FROM t_order ORDER BY refund_time ASC NULLS FIRST; -- 升序且NULL排最后金仓默认 SELECT * FROM t_order ORDER BY refund_time ASC NULLS LAST;把NULLS FIRST / NULLS LAST写清楚行为就不会因为数据库不一样而变化。还有一个字符串比较的差异MySQL在部分排序规则下会忽略字符串尾部的空格比如abc abc 是成立的金仓默认行为下这两个字符串不相等。如果业务有用固定长度字符做比对或去重的逻辑迁移后可能会查出和以前不一样的结果。我们当时就有一张用户表某些字段因为历史原因填充了尾部空格MySQL查出来的数据和金仓查出来的对不上排查半天才定位到是空白符比较差异。3.2 常用函数替换对照表函数差异是SQL改写投入最大的部分。我把这次迁移中遇到的所有常用函数差异整理成了对照表按这个表改就能覆盖大部分场景功能MySQLKingbaseES补充说明空值替换IFNULL(a, 0)COALESCE(a, 0)COALESCE是标准SQL支持多个参数条件判断IF(a b, a, b)CASE WHEN a b THEN a ELSE b END也可以用金仓的IIF但CASE最通用日期格式化DATE_FORMAT(d, %Y-%m-%d)TO_CHAR(d, YYYY-MM-DD)格式符完全不同需要逐个转换字符串转日期STR_TO_DATE(s, %Y-%m-%d)TO_DATE(s, YYYY-MM-DD)格式符体系和上面一样字符串截取SUBSTRING_INDEX(s, ., 1)SPLIT_PART(s, ., 1)SPLIT_PART是金仓里的常用函数分组拼接GROUP_CONCAT(x)STRING_AGG(x, ,)也可用LISTAGG但STRING_AGG更稳字符串连接CONCAT(a, b, c)CONCAT(a, b, c) 或 a || b注意Oracle模式下||对NULL的处理正则匹配s REGEXP patterns ~ pattern~*表示不区分大小写匹配获取当前时间NOW()NOW() 或 CURRENT_TIMESTAMP两边都通用问题不大取日期部分DATE(d)CAST(d AS DATE) 或 d::DATE金仓也支持DATE(d)POSIX风格不一定都兼容我在实际改写时有一个习惯先全局搜索IFNULLIF(DATE_FORMATGROUP_CONCATREGEXP这些MySQL特征明显的函数然后逐个替换成上面的标准写法。搜索关键字替换一遍之后再跑一遍应用的测试用例能过滤掉大部分兼容性问题。这里单独说一下GROUP_CONCAT。MySQL的GROUP_CONCAT(x ORDER BY y SEPARATOR ,)写法在金仓里对应的是STRING_AGG(x, , ORDER BY y)。顺序和分隔符的位置都不一样转换时容易出错。-- MySQL SELECT dept_id, GROUP_CONCAT(user_name ORDER BY user_id SEPARATOR ,) FROM t_user GROUP BY dept_id; -- KingbaseES SELECT dept_id, STRING_AGG(user_name, , ORDER BY user_id) FROM t_user GROUP BY dept_id;如果你在建表时用的是金仓的Oracle兼容模式也可以使用Oracle风格的LISTAGG(x, ,) WITHIN GROUP (ORDER BY y)。但为了团队好维护我建议整篇代码统一用一种风格不要混用。3.3 插入冲突与覆盖写法的差异MySQL里有一个非常常用的语法INSERT ... ON DUPLICATE KEY UPDATE用于在记录已存在时执行更新。这个写法金仓不支持需要改写成PostgreSQL风格的INSERT ... ON CONFLICT-- MySQL INSERT INTO t_user (id, name, age) VALUES (1, 张三, 20) ON DUPLICATE KEY UPDATE age VALUES(age); -- KingbaseES INSERT INTO t_user (id, name, age) VALUES (1, 张三, 20) ON CONFLICT (id) DO UPDATE SET age EXCLUDED.age;两个细节要注意。第一ON CONFLICT后面必须指定冲突列这个列上要有唯一索引或主键约束否则没法判断冲突。第二EXCLUDED.age表示本次插入想写入的那一行数据这对应MySQL侧的VALUES(age)。很多人在这里会写错直接写成t_user.age结果更新被旧值覆盖数据一直没有变化排查起来很隐蔽。MySQL的REPLACE INTO也是特征明显的语法。它的逻辑是先尝试插入如果主键或唯一键冲突就先删除旧记录再插入新记录。金仓里没有对应的REPLACE INTO我们统一改写成INSERT ... ON CONFLICT的形式。还有一个容易被忽视的点MySQL默认UPDATE语句如果影响行数为0不会报错而金仓的扩展语法UPDATE ... FROM在某些写法下可能因为连接条件产生多条更新实际更新行数会超出预期。这类问题不会直接报错但会导致数据不一致建议在做数据比对时把更新类SQL也纳入核对范围。4. 存储过程、触发器与事务行为差异4.1 存储过程的声明和结构差异存储过程是迁移中最费时间的一块因为MySQL的存储过程语法和金仓完全不是一个体系。MySQL里用DELIMITER改变语句分隔符用BEGIN...END包裹过程体金仓默认使用PL/pgSQL风格过程体放在AS $$ ... $$之间也可以使用Oracle兼容模式下的IS ... BEGIN ... END写法。看一个最简单的例子-- MySQL 存储过程 DELIMITER $$ CREATE PROCEDURE sp_get_user_count(OUT cnt INT) BEGIN SELECT COUNT(*) INTO cnt FROM t_user; END$$ DELIMITER ;-- KingbaseES 存储过程 CREATE OR REPLACE PROCEDURE sp_get_user_count(OUT cnt INT) AS $$ BEGIN SELECT COUNT(*) INTO cnt FROM t_user; END; $$ LANGUAGE plpgsql;这里的差异点包括MySQL里声明参数和变量直接用DECLARE金仓的参数直接写在括号里变量在DECLARE段声明而且金仓的变量赋值用:不能用SET。MySQL的存储过程体里每条SQL都以分号结尾但整个过程用DELIMITER避免冲突金仓用$$包裹过程体不需要改分隔符。金仓存储过程末尾最好显式指定LANGUAGE plpgsql避免某些环境下默认语言解析出错。如果组里还有人把存储过程写成Oracle风格比如用CREATE OR REPLACE PROCEDURE ... IS ... BEGIN ... END;那对金仓来说需要处于Oracle兼容模式下才能跑通。所以这里必须有一个统一约定要么全用PL/pgSQL风格要么全用Oracle风格混写会非常痛苦。4.2 游标与异常处理游标这一块MySQL的经典写法是DECLARE OPEN FETCH CLOSE金仓里当然也支持这种写法但更简洁的方式是用FOR循环直接遍历查询结果。-- MySQL 游标 DECLARE cur CURSOR FOR SELECT id FROM t_user; OPEN cur; FETCH cur INTO v_id; WHILE done 0 DO -- 业务处理 FETCH cur INTO v_id; END WHILE; CLOSE cur;-- KingbaseES 游标 FOR v_id IN SELECT id FROM t_user LOOP -- 业务处理 END LOOP;用FOR循环的方式金仓会自动打开、遍历并关闭游标代码量一下少了一半。当然PL/pgSQL里也有OPEN/FETCH/CLOSE的显式游标写法但既然能用简洁的方式为什么不用。异常处理差异也很典型。MySQL里写异常处理是通过DECLARE...HANDLER比如DECLARE CONTINUE HANDLER FOR SQLEXCEPTION金仓则在BEGIN...EXCEPTION...END块里处理-- KingbaseES 异常处理 BEGIN -- 业务代码 EXCEPTION WHEN others THEN -- 异常处理 RAISE NOTICE error occurred; END;金仓的异常类型比MySQL丰富比如WHEN unique_violation、WHEN foreign_key_violation等。如果原来的存储过程里只笼统地捕获SQLEXCEPTION迁移后可以按业务需求拆分成更具体的异常分支这属于迁移过程中的顺手优化。4.3 事务与DDL回滚行为事务行为上MySQL有个特点DDL语句会隐式提交当前事务。也就是说你在一个事务里执行了UPDATE、再执行ALTER TABLE这个UPDATE就被自动提交了后面想ROLLBACK整体回滚是做不到的。金仓则不同它支持事务内的DDL回滚至少断开连接前DDL可以作为事务的一部分回滚。这对应用意味着什么如果有代码依赖“DDL之后前面的操作不可回滚”这种MySQL特性迁移后行为会不一样。不过大多数业务系统不会刻意利用这一点了解即可。另外一个实际影响比较大的点是金仓的DDL事务性会让一些迁移脚本的行为发生变化。比如你在一个事务里先建表、再插数据、再回滚MySQL里建表语句已经悄悄提交了表会留下来金仓里整个事务回滚表也不会存在。所以用脚本批量执行迁移时务必明确脚本是否会执行COMMIT否则很容易出现“数据没进去、表也没了”的困惑。5. 从MySQL迁移到金仓的实操流程5.1 迁移工具选型我们在迁移前先调研了工具发现金仓本身提供了官方的数据迁移工具叫做KDTSKingbase Data Transfer Studio支持从MySQL、Oracle等多种数据库迁移到金仓。这个工具可以连源库抽表结构、抽数据、生成目标库的对象定义还能做一部分SQL语法转换。但这里要提醒一句工具能解决的只是“机械转换”业务逻辑里复杂的SQL改写还是得靠人。除了金仓官方工具通用的ETL工具如Kettle、DataX也能做数据同步。我们的主迁移路径用的是官方工具处理表结构用DataX做大数据量的数据搬迁最后再手工处理视图、存储过程、触发器。表格可以对比一下工具适用阶段优点注意点KDTS表结构、数据、部分对象转换官方支持、图形化复杂存储过程转换成功率不高DataX数据全量/增量同步稳定、支持断点续传需要额外部署执行环境Kettle数据同步、清洗灵活大批量性能一般手工SQL脚本视图、函数、触发器可控耗时依赖人员熟悉度工具一定选带联机评估的让工具先跑一次评估报告它能列出哪些对象能转、哪些不能转。这份报告就是你后续安排人力的依据。5.2 分阶段迁移步骤我们的迁移步骤拆成五个阶段每个阶段都有独立验收标准第一阶段是评估。先收集源库所有对象的清单表、字段、索引、主外键、视图、函数、存储过程、触发器甚至包括定时事件。把对象清单和依赖关系画清楚明确哪些可以自动转换哪些必须手工处理。这个阶段输出的就是迁移评估报告。第二阶段是表结构迁移。用KDTS自动生成建表语句然后按前面说的差异点逐一核对重点检查自增列、默认值、注释、时间类型。表结构确认无误后先建到目标金仓库这时候不急着导数据。第三阶段是数据迁移。全量数据用DataX先导一遍导完分别统计源库和目标库的表行数做比对。如果是停机迁移就全量一次性搬如果要求不停机就得做增量同步源库MySQL开binlog用同步组件把增量数据继续同步到金仓。我们这次为了控制风险采用的是夜间停机窗口全量搬迁完之后第二天业务直接切到金仓。第四阶段是对象脚本迁移。把视图、存储过程、触发器、函数这些从MySQL的SHOW CREATE VIEW或者mysqldump导出来先经过人工改写再到目标库执行。这个阶段最容易出现语法错误我建议每改写一个就单独执行测试不要攒一堆一起执行否则报错时都不知道是哪一个的问题。第五阶段是功能验证。对应用做全量接口回归核心接口最好有自动化测试。同时做一次源库和目标库的数据一致性比对重点看几个容易出问题的字段比如时间类型、布尔类型、浮点类型。发现问题回到对应环节修复直到所有用例通过。5.3 应用代码层的适配重点数据库层迁移完成后应用代码也要跟着改这部分容易被低估。我们的经验是重点关注这几个地方连接配置驱动从com.mysql.cj.jdbc.Driver换成金仓的驱动连接URL协议变化这个属于基础操作。MyBatis里的方言SQL所有手写SQL都要过一遍重点是分页、函数、自增主键回填。MyBatis的useGeneratedKeystrue和keyPropertyid在MySQL下可以用到了金仓如果走的是SERIAL或IDENTITY用selectKey方式回填主键更稳妥insert idinsertUser selectKey resultTypejava.lang.Integer orderBEFORE keyPropertyid SELECT nextval(t_user_id_seq) /selectKey INSERT INTO t_user (id, name, age) VALUES (#{id}, #{name}, #{age}) /insert这个写法对序列的依赖很强所以前面提到的序列值初始化必须在应用切换前完成否则这里就会拿到重复的ID。SQL规范化给团队定一个开发规范新写的SQL尽量避免使用数据库特有函数能用标准SQL就写标准SQL表名、字段名统一用小写避免双引号不要依赖MySQL的隐式类型转换特性。我们团队在迁移后定了一条规矩凡是在代码里出现的SQL必须同时能在MySQL和金仓上跑通才允许提交。6. 迁移中常见问题与排查技巧实录6.1 高频报错速查表迁移过程中遇到十几个报错我把最高频的整理成了一张表方便你排查时直接对照错误现象根本原因解决方案LIMIT附近有语法错误MySQL分页顺序和金仓不一致改成LIMIT count OFFSET offsetrelation t_user does not exist大小写问题统一小写表名避免双引号function date_format does not exist函数不兼容替换成TO_CHAR并调整格式符column xxx must appear in GROUP BYMySQL宽松分组和金仓严格分组不同用DISTINCT ON或改写分组逻辑INSERT has more expressions than target columns自增列默认值冲突插入时排除自增列或用DEFAULTinvalid input syntax for type timestamp时间字符串格式差异用TO_TIMESTAMP转换后再插入feature not supported in this mode用了Oracle模式不支持的特性切换兼容模式或改写语法这些报错里最坑的是GROUP BY相关错误。MySQL默认的ONLY_FULL_GROUP_BY是关闭状态5.7之后默认开启但很多老项目还开着宽松模式允许select列不包含在group by里。金仓严格按照SQL标准select列必须出现在group by中或聚合函数里。解决办法是改写SQL把不需要分组的列用聚合函数包起来或者调整查询逻辑。6.2 大小写和字符集引发的诡异问题大小写问题比想象中更隐蔽。MySQL在Linux下表名区分大小写在Windows下不区分金仓默认对表名、字段名忽略大小写但如果你给对象名加了双引号它就变成一个区分大小写的标识符。举个例子开发在MySQL里写了一句SELECT * FROM T_USERMySQL在Windows环境不区分大小写能正常查出来到了金仓如果T_USER被双引号包住它就会去精确找大写名称的表结果relation不存在。我们的建议是全项目SQL统一用小写表名并且不要加双引号。如果是从代码里动态拼接表名的检查拼接逻辑里有没有硬转大写的动作。字符集问题则集中在中文乱码上。金仓建库时默认字符集一般是UTF8如果连接串里没有指定字符集或者数据库本身的字符集是GBK之类导进去的中文就会乱。连接串里建议显式加上字符集参数比如?characterEncodingUTF-8具体参数名取决于驱动版本。同时批量导入时设置客户端的client_encoding和数据库实际编码保持一致。这是我们实际遇到的一个案例某张表有备注字段源库是utf8mb4目标库金仓创建时用默认编码没问题但DataX同步时没有指定编码结果一列文本全部变成了问号。后来在DataX的jdbcUrl里加上useUnicodetruecharacterEncodingutf8重新同步一次才解决。6.3 执行计划与统计信息迁移数据之后还有一个经常被忽略的问题统计信息没有更新。MySQL在导入大量数据后InnoDB会逐步更新统计信息金仓依赖优化器的统计信息做执行计划。如果数据导入后从没执行过ANALYZE优化器可能还在用空表的统计信息导致生成的执行计划很差甚至出现全表扫描。我处理慢SQL的步骤是先用EXPLAIN看执行计划再用ANALYZE更新统计信息最后再看一次执行计划。金仓的EXPLAIN输出和MySQL差别很大MySQL里有type、key、rows这些指标金仓里则是Seq Scan、Index Scan、cost...这些。如果你看到一条SQL走了Seq Scan而表数据量又不小大概率就是缺索引或者统计信息不准。EXPLAIN ANALYZE SELECT * FROM t_order WHERE user_id 100;返回结果里如果出现Seq Scan on t_order说明没有走索引。先检查有没有建索引再执行ANALYZE t_order;然后再看执行计划。这一步对迁移后的稳定性能提升非常关键。另外金仓里表数据量大的时候要考虑刷新统计信息的频率。我们有几张千万级流水表在导完数据后立刻执行全表ANALYZE加索引重建执行计划才恢复正常。这点务必写进迁移验收清单。结尾这次迁移让我最深刻的体会是SQL语法差异看起来是细节问题但恰恰是这些细节决定了整个迁移是否顺利。与其等测试阶段被各种报错淹没不如在迁移前就把差异清单发给团队让每个开发改SQL时都有据可查。上面整理的这些MySQL与人大金仓差异点基本覆盖了我们在这次迁移中遇到的所有高频问题你现在做迁移可以直接拿来当检查表用。最后再分享一个实用小技巧在改写SQL时不要只改语法顺手把SQL规范统一一下比如表名字段名不加引号、统一小写、把变化大的函数封装成数据库视图或者应用层统一处理。这样以后再做类似数据库切换改动量会小很多。代码整洁的团队从来不是在切换数据库那一刻才开始规范SQL而是在日常开发里就尽量避免对某一种数据库的强依赖。