ARTICLE DETAIL

资讯详情

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

MySQL迁移人大金仓SQL语法差异详解:自增列、分页与函数改写实践

MySQL迁移人大金仓SQL语法差异详解:自增列、分页与函数改写实践 说实话国产数据库迁移这件事最磨人的往往不是数据量而是那些看起来一样、跑起来报错的SQL。我接手过好几个从MySQL迁到人大金仓KingbaseES的项目不少开发同学在MySQL下写得很顺的查询和存储过程切到金仓后连最基本的INSERT都过不去第一反应就是“这数据库兼容性不行”。兼容性确实有区别但更多时候是我们习惯了MySQL的“方言语法”没有搞清楚金仓的SQL底子到底长什么样。这篇文章基于我几次迁移的实测经验专门梳理MySQL和人大金仓在SQL语法上的高频差异点附上可以直接对照修改的写法给正在做迁移评估、改造方案或者准备KCA相关认证考试的同学做个参考。1. 兼容模式不等于免费迁移先搞清楚金仓的SQL底子人大金仓KingbaseES有一个很吸引人的特性支持多种兼容模式包括Oracle模式、MySQL模式、PostgreSQL模式。很多朋友听到“兼容MySQL”就以为把数据库的模式切过去原来那套SQL就能直接跑起来。实际情况远没有这么乐观。KingbaseES的底层架构和SQL引擎继承了大量PostgreSQL的设计思路尤其表现在这几个方面标识符大小写规则、类型系统的严格程度、函数和操作符的语义。即便开启了MySQL兼容模式数据库也只是在语法解析层尽量识别MySQL的关键字和习惯写法但底层的数据类型、函数映射、约束行为并不会完全等价改写。举个实际例子。MySQL里执行SELECT TRUE AND 1;很自然因为TINYINT(1)和布尔值混用是常态。但在金仓的PostgreSQL内核语义下BOOLEAN是独立类型和整数之间没有隐式转换那类写法照样会报错。再比如MySQL的LIMIT 10, 20这种逗号分页写法在兼容模式下可能能用换到Oracle模式或标准PG语义下就直接语法错误。所以我一直和团队说一句话迁移不是“模拟运行”而是“重写适配”。不要指望自动翻译工具把所有SQL原封不动地搬过去先把两边SQL的关键差异摸清楚再配合工具批量扫描改写这才是真正省时间的路子。提示金仓兼容模式解决的是“能不能认识你的语法”不代表“所有函数和类型都能一一对应”。做迁移方案时一定要把“SQL改写”纳入工作量和风险清单里而不是只测数据导入。2. 字段类型对照看似一样存储规则却差很远2.1 常用字段类型映射表字段类型是最先碰到的一层差异。很多表结构在迁移工具下能跑通但含义已经悄悄变了。下面这张表是我在实际项目中经常用到的对照关系做结构迁移的时候可以按这个基线来改MySQLKingbaseES推荐写法说明INT / INTEGERINTEGER 或 INT基本一致BIGINTBIGINT基本一致TINYINTSMALLINT金仓没有TINYINT按SMALLINT处理TINYINT(1)BOOLEAN 或 SMALLINT如果只存0/1可转BOOLEAN否则保持SMALLINTDECIMAL(M, D)DECIMAL(M, D) 或 NUMERIC(M, D)语义基本一致VARCHAR(N)VARCHAR(N)都按字符数计算注意字符集影响存储CHAR(N)CHAR(N)基本一致TEXTTEXT金仓原生支持BLOB / LONGBLOBBYTEA对应PG风格二进制类型DATETIMETIMESTAMP注意时区和精度TIMESTAMPTIMESTAMP WITH TIME ZONE两者语义差别较大DATEDATE基本一致JSONJSONB推荐用JSONB性能更好ENUM不直接支持建议改为CHECK约束加VARCHAR或自定义类型但工程上建约束更简单这张表只是起点。真正麻烦的是MySQL里那些“修饰符”和隐含行为下面展开说。2.2 MySQL独有修饰符unsigned、zerofill和显示宽度MySQL建表时很流行写INT(10) UNSIGNED或BIGINT(20)这类写法在金仓里是不被识别的。金仓的整数类型没有“显示宽度”的概念也不区分 signed 和 unsigned直接用INT、BIGINT表达即可。这就带来一个实际风险如果原来表结构里INT UNSIGNED是用来存储很大的正整数那么迁移到金仓后同等长度下确实够用因为这个范围是一样的。但如果你从工具自动生成的建表语句里看到INT并不代表范围一定对齐了最好专门检查一下源端的 unsigned 字段避免后续插入超出范围引发报错。另外MySQL的ZEROFILL填充效果在迁移后就不用指望了建议在应用层或查询层用LPAD函数去实现不要在数据库结构层面再做这个依赖。2.3 时间类型和字符集的隐性差异时间类型是个高频踩坑点。MySQL里DATETIME和TIMESTAMP差别很大TIMESTAMP受时区影响DATETIME不受时区影响范围也不一样。金仓里默认推荐用TIMESTAMP但这其实对应PG风格的TIMESTAMP WITHOUT TIME ZONE如果你要的是“带时区”的语义必须显式写成TIMESTAMP WITH TIME ZONE。字符集方面MySQL 8.0 默认是utf8mb4金仓更习惯用UTF8实际上PG的UTF8对应MySQL的utf8mb4排序规则也各有不同。如果原来表结构里用了COLLATE utf8mb4_unicode_ci这类写法迁移工具大概率不认识需要手动去掉并在数据库初始化时把字符集统一成 UTF8否则中文数据进去后统计排序、唯一索引这些行为都可能出现偏差。3. 自增列与序列最容易被坑的“主键生成”方案3.1 AUTO_INCREMENT为什么不能直接搬MySQL建表时写自增主键已经成了默认习惯比如CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这套在MySQL里跑得很顺因为AUTO_INCREMENT是MySQL表属性的一部分数据库自动帮你维护“下一个值”。但人大金仓里没有和AUTO_INCREMENT完全等价的语法机制。金仓处理自增的方式更接近PostgreSQL——用“序列SEQUENCE”或者GENERATED BY DEFAULT AS IDENTITY语法来实现。如果你直接拿迁移工具把上面的表结构导过去很多工具会把id字段生成为一个普普通通的INTEGER NOT NULL没有任何默认值。结果就是插入数据的瞬间报错提示主键字段没有默认值具体报错会写在后面第七章的案例里。3.2 金仓下三种自增实现在金仓里实现自增主键我推荐三条路按优先级排序第一种直接用序列伪类型最简单直接CREATE TABLE user ( id BIGSERIAL PRIMARY KEY, name VARCHAR(64) NOT NULL );这种写法会自动创建一个序列并把默认值挂到id列上插入时不用手动填id数据库会自动生成。第二种标准IDENTITY语法更接近SQL标准CREATE TABLE user ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(64) NOT NULL );区别在于GENERATED BY DEFAULT允许用户显式插入id而GENERATED ALWAYS拒绝用户插入值。工程上推荐BY DEFAULT因为做数据迁移时你还需要把老表的历史id一起搬进去ALWAYS会挡着你。第三种显式创建序列并绑定到列上CREATE SEQUENCE user_id_seq; CREATE TABLE user ( id BIGINT NOT NULL DEFAULT nextval(user_id_seq) PRIMARY KEY, name VARCHAR(64) NOT NULL );这种写法在复杂场景下更灵活比如多个表共享一个序列、或者需要手工控制序列当前值。但单表自增主键的场景前两种已经够了。注意在MySQL兼容模式下部分版本的KingbaseES也能够识别AUTO_INCREMENT关键字并自动转为序列。但我不建议把项目长期建立在这个兼容行为上因为你不知道后续换库、换模式还会不会保留这类支持。统一改成上面的标准写法以后去哪都不怕。3.3 获取插入后ID的方式差异MySQL里获取刚插入记录的自增ID最常用的是INSERT INTO user (name) VALUES (张三); SELECT LAST_INSERT_ID();金仓里获取刚插入ID的方式不太一样。第一种是使用RETURNING子句这也是PG系最有用的特性之一INSERT INTO user (name) VALUES (张三) RETURNING id;第二种方式是用CURRVAL查看序列当前值SELECT currval(user_id_seq);需要注意currval()只能在当前会话执行过nextval()之后调用如果会话里还没有用过这个序列直接调用会报错。使用RETURNING就没有这个限制这也是我强烈推荐在应用层改用RETURNING的原因。现在JDBC里很多驱动也支持RETURNING取主键比老方案里专门查一次LAST_INSERT_ID()更优雅。4. 查询语句差异分页、去重、空值与布尔逻辑4.1 分页写法一种是能用一种是一定要改MySQL分页最常见的是LIMIT offset, count这种写法SELECT id, name FROM user ORDER BY id LIMIT 10, 20;在PartgreSQL和Oracle风格模式下这种带逗号的LIMIT分页是语法错误金仓也一样。迁移时应该统一改成标准写法SELECT id, name FROM user ORDER BY id OFFSET 10 LIMIT 20;或者使用SQL标准写法SELECT id, name FROM user ORDER BY id OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY;虽然KingbaseES在MySQL兼容模式下可能还能勉强认LIMIT 10, 20但我强烈建议项目里统一使用LIMIT ? OFFSET ?这种写法因为它在PG系数据库里语义最清晰也不会触发兼容模式的潜在bug。实操经验做迁移扫描时直接正则搜代码里的LIMIT \d,\d这个模式基本能把所有需要改的分页SQL一次性扫出来。比人工翻代码快得多。4.2 IFNULL与IF函数怎么改MySQL里空值处理非常随意IFNULL、IF满天飞。比如SELECT IFNULL(name, 无名) FROM user; SELECT IF(age 18, 成年, 未成年) FROM user;金仓里更推荐标准的COALESCE和CASE WHENSELECT COALESCE(name, 无名) FROM user; SELECT CASE WHEN age 18 THEN 成年 ELSE 未成年 END FROM user;这里有一个容易被忽视的差异IFNULL只接受两个参数COALESCE可以接受多个参数而且会从左到右取第一个非NULL值功能上完全覆盖IFNULL。至于IF函数金仓虽然有部分环境在兼容模式下能识别但不要依赖老老实实改成CASE WHEN更稳妥。4.3 DISTINCT ON是个金仓特色能力反过来PG系里有一个MySQL完全没有的语法DISTINCT ON。它可以在按某列分组的同时直接选出每组里某个排序条件下的第一行SELECT DISTINCT ON (user_id) user_id, order_id, created_at FROM orders ORDER BY user_id, created_at DESC;这个语法在取“每个用户最新订单”之类的场景里非常顺手。MySQL里没有等价写法只能用子查询加窗口函数。如果你是从MySQL过来的开发写金仓SQL时遇到“每组取最新一条”的需求可以试试这个语法比窗口函数写得短执行计划也往往更直接。4.4 布尔值与TINYINT(1)的坑MySQL里布尔值本质上就是TINYINT(1)你写WHERE is_active 1和WHERE is_active TRUE效果一样。金仓有原生BOOLEAN类型TRUE和FALSE是独立的布尔值和整数不能直接比较。迁移时如果原来字段是TINYINT(1)要么改成BOOLEAN并同步改写SQL里的查询条件要么维持SMALLINT只把代码里的TRUE/FALSE映射为1/0。我建议如果字段语义很明确是“开关状态”直接改成BOOLEAN把SQL统一写成WHERE is_active IS TRUE这种形式类型更干净。如果字段里还存了其他非0/1的值那就保持SMALLINT别强行转布尔。5. 字符串处理与聚合函数写法不同结果也不完全相同5.1 高频函数对照表字符串和聚合函数是SQL改写中数量最多的部分我整理了一份高频对照表平时写代码和review的时候可以直接用来查场景MySQL写法KingbaseES推荐写法空值替代IFNULL(col, val)COALESCE(col, val)条件判断IF(cond, a, b)CASE WHEN cond THEN a ELSE b END行转列聚合GROUP_CONCAT(col)STRING_AGG(col, ,)字符串拼接CONCAT(a, b)CONCAT(a, b) 或 a || b取子串SUBSTRING(str, pos, len)SUBSTRING(str, pos, len) 基本兼容按分隔符截取SUBSTRING_INDEX(str, ,, n)SPLIT_PART(str, ,, n)日期格式化DATE_FORMAT(d, %Y-%m-%d)TO_CHAR(d, YYYY-MM-DD)正则匹配REGEXP~ 或 REGEXP_LIKE字符串长度CHAR_LENGTH(str)CHAR_LENGTH(str) 或 LENGTH(str)去空格TRIM(str)TRIM(str) 基本一致5.2 GROUP_CONCAT变成STRING_AGG后要注意什么GROUP_CONCAT改成STRING_AGG不是简单换函数名有几个细节必须注意。首先是分隔符。MySQL里GROUP_CONCAT(col)默认逗号分隔如果需要其他分隔符写SEPARATORSELECT GROUP_CONCAT(name SEPARATOR 、) FROM user GROUP BY dept_id;金仓里要用STRING_AGG的第二个参数显式指定SELECT STRING_AGG(name, 、) FROM user GROUP BY dept_id;其次是结果排序。MySQL可以在聚合内指定排序GROUP_CONCAT(name ORDER BY id SEPARATOR ,)。金仓同样支持STRING_AGG(name, , ORDER BY id)。最后是长度限制。MySQL的GROUP_CONCAT默认有group_concat_max_len限制默认1024一长就截断很容易踩坑。金仓的STRING_AGG没有这个固定限制反而更省心。5.3 日期格式化与正则匹配日期格式化是代码扫描里面最容易出问题的地方。MySQL写%Y-%m-%d金仓写YYYY-MM-DD格式符体系完全不一样-- MySQL SELECT DATE_FORMAT(created_at, %Y-%m-%d %H:%i:%s) FROM orders; -- KingbaseES SELECT TO_CHAR(created_at, YYYY-MM-DD HH24:MI:SS) FROM orders;正则匹配也是重灾区。MySQL里正则匹配用REGEXP关键字比如WHERE name REGEXP ^张。金仓里更PG风格的是用~操作符-- MySQL SELECT * FROM user WHERE name REGEXP ^张; -- KingbaseES SELECT * FROM user WHERE name ~ ^张;如果你的SQL里同时依赖正则替换MySQL是REGEXP_REPLACE(str, pattern, replacement)金仓也支持函数形态但参数顺序和返回类型略有差别建议单独验证后再上线。5.4 严格分组带来的兼容压力MySQL在默认配置下SELECT中出现的非聚合列可以不在GROUP BY里比如SELECT name, age FROM user GROUP BY dept_id在MySQL里能跑但取哪个name是随机的。金仓的默认行为更严格非聚合列必须出现在GROUP BY里否则直接报错。迁移时遇到这种SQL不能只加一个GROUP BY字段了事要先想清楚业务到底要什么。如果只是想按部门取一个name建议用DISTINCT ON、窗口函数或者子查询而不是靠MySQL的宽松语义去“碰运气”。6. 存储过程与事务控制从SQL到过程化编程的断层6.1 存储过程的骨架差异MySQL的存储过程写法很固定大家熟知的DELIMITERBEGIN...END那套DELIMITER // CREATE PROCEDURE add_user(IN uname VARCHAR(64)) BEGIN INSERT INTO user (name) VALUES (uname); END // DELIMITER ; CALL add_user(张三);金仓里的存储过程写法更接近Oracle和PG的混合体。如果用PL/pgSQL风格默认写法是CREATE OR REPLACE PROCEDURE add_user(uname VARCHAR(64)) LANGUAGE plpgsql AS $$ BEGIN INSERT INTO user (name) VALUES (uname); END; $$; CALL add_user(张三);两者最大的区别是MySQL写过程体不需要指定语言金仓需要LANGUAGE plpgsqlMySQL用DELIMITER处理结束符冲突金仓用美元引用$$包住过程体天然避免这个问题。6.2 错误处理与RETURNING错误处理方面差异也很明显。MySQL里常见的异常处理是DECLARE EXIT HANDLERDECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END;金仓PL/pgSQL的写法则完全不同EXCEPTION WHEN others THEN ROLLBACK; RAISE; END;简单来说MySQL把异常处理器声明在DECLARE段金仓把异常块放在BEGIN...EXCEPTION...END结构里。逻辑从“声明一个处理器”变成了“把可能出错的代码包在异常块里”。另外在金仓的存储过程或函数里RETURNING子句也很好用尤其在过程里插入数据后想直接拿到生成的主键时CREATE OR REPLACE FUNCTION add_user(uname VARCHAR(64)) RETURNS BIGINT LANGUAGE plpgsql AS $$ DECLARE v_id BIGINT; BEGIN INSERT INTO user (name) VALUES (uname) RETURNING id INTO v_id; RETURN v_id; END; $$;这套写法和MySQL里先INSERT再SELECT LAST_INSERT_ID()的方式相比少一次查询也更原子。6.3 事务和隔离级别的默认值不同MySQL InnoDB默认隔离级别是REPEATABLE READ而且默认autocommit1。金仓PG内核默认隔离级别是READ COMMITTED同样默认自动提交。这个差异看起来不大实际影响不小。如果你的应用做了“在一个事务里多次查询同一张表期望结果完全一致”的逻辑MySQL的RR隔离级别下MVCC机制能保证快照读的一致性而金仓默认的RC下同一个事务里第二次查询可能看到其他会话已提交的新数据。如果迁移后业务依赖这个一致性两种处理办法第一种把事务隔离级别显式设置成REPEATABLE READSET TRANSACTION ISOLATION LEVEL REPEATABLE READ;第二种在金仓里用BEGIN包住更长的事务并按PG的MVCC语义重新审视查询逻辑很多情况下应用层调整一下查询顺序就能避免问题。DDL的隐式提交也值得注意。MySQL里DDL语句建表、删表、ALTER会隐式提交当前事务。金仓里DDL是支持事务回滚的意味着在一个事务里建表后回滚表也会被撤掉。这个特性其实更灵活但如果你原来写的脚本依赖“DDL不能回滚”的MySQL行为迁到金仓后反而要注意别把DDL放进事务里。7. 两次真实排错记录从报错到结论的完整链路7.1 案例一主键导入后没有默认值有一次迁移一个订单系统源端MySQL表结构里主键写的是id int NOT NULL AUTO_INCREMENT迁移工具跑完建表和数据之后应用联调时一插入数据就报错错误信息类似ERROR: null value in column id violates not-null constraint我当时的排查链路是这样的第一步先看金仓里的表定义确认id列长什么样。执行\d orders检查后发现id被迁移成了integer NOT NULL没有任何默认值也没有关联序列。第二步查迁移日志发现工具只搬运了字段类型和约束没有把MySQL的AUTO_INCREMENT语义翻译成金仓的序列对象。这一步基本确认了问题的根因。第三步手动补上序列并绑定默认值CREATE SEQUENCE orders_id_seq; ALTER TABLE orders ALTER COLUMN id SET DEFAULT nextval(orders_id_seq); SELECT setval(orders_id_seq, COALESCE((SELECT MAX(id) FROM orders), 1));这里有个细节setval这步很关键因为老表里可能已经有大量历史数据如果不把序列起点跳到当前最大值后面新插入的数据会和老数据的主键撞上。第四步重新执行一条INSERT插入成功再查一次序列当前值确认ID是连续生成的。这个问题在金仓迁移项目里出现频率极高。现在我的习惯是迁移任何MySQL表结构之前先全局扫一遍AUTO_INCREMENT字段然后统一在目标端改成BIGSERIAL或GENERATED BY DEFAULT AS IDENTITY不要等迁移工具生成完再回头补。7.2 案例二表名大小写引发的“关系不存在”另一个项目里源端MySQL有一张表叫UserInfo代码里SQL的写法也一直是SELECT * FROM UserInfo。MySQL里表名大小写在Linux服务器上虽然敏感但大多数人建表习惯统一所以这里没出过问题。迁到金仓之后应用日志里开始报ERROR: relation userinfo does not exist排查链路比较绕我记录一下。第一步我以为是表没迁过来先到金仓里\dt查看表列表发现表名确实存在叫UserInfo只是带着双引号。第二步确认是标识符大小写规则的问题。KingbaseES遵循PG规则不带双引号的标识符会被折叠成小写。也就是说SQL里写的UserInfo实际会被解析成userinfo而建表时迁移工具用了带引号的UserInfo两者匹配不上自然报“不存在”。第三步决定修复方案。两条路一条是把所有SQL改成SELECT * FROM UserInfo但这样代码里到处是引号后续维护很容易漏Java里的SQL字符串也会很难看另一条是把表名统一改成小写比如user_info同时把代码里的引用全部改掉。我选了后者并且要求团队后续所有建表、字段命名全部统一小写加下划线不做任何大小写混合。这个案例之后我也吸取了一个教训迁移前就要统一约定“标识符命名规范”不要给迁移工具留下生成大小写混合表名的机会。建表语句里最好全部使用小写不要带双引号这样在金仓里最安全。8. 迁移前的语法体检清单与辅助工具8.1 最值得提前扫出来的语法点基于前面这些差异我每次迁移前都会做一轮“语法体检”目标是把需要改动的SQL提前暴露出来。下面是经常用到的检查项清单检查项正则/搜索关键词说明AUTO_INCREMENTauto_increment全部改成SERIAL或IDENTITY逗号分页LIMIT\s\d\s*,\s*\d改成 LIMIT ? OFFSET ?IF函数\bIF\(改成 CASE WHENIFNULL函数IFNULL\(改成 COALESCEGROUP_CONCATGROUP_CONCAT\(改成 STRING_AGG 并检查排序DATE_FORMATDATE_FORMAT\(改成 TO_CHAR 并检查格式符正则REGEXPREGEXP改成 ~ 或 REGEXP_LIKE反引号反引号字符建表SQL里的反引号要全部去掉TINYINT(1)TINYINT\(1\)确认是否转BOOLEAN大小写混合表名大写字母出现在表名统一改成小写这套清单我现在都做成了脚本扫描SQL文件和代码里的字符串输出一份Excel给开发团队逐条改。比靠人肉review可靠得多。8.2 用起来比较顺手的验证工具语法改完之后光靠静态检查还不够一定要有环境可以做动态验证。我常用的方式是在本地用Docker起一个KingbaseES测试实例网上也能找到相关镜像和部署文档把改完的建表脚本和核心SQL在新的实例上跑一遍重点看执行计划和返回结果是否和MySQL一致。客户端工具方面像dbx这类支持多数据库连接的工具也很好用。一个窗口连MySQL一个窗口连金仓把同样一条SQL分别执行结果和报错一目了然做差异分析特别方便。另外如果项目里有ORM框架比如MyBatis建议在代码层做一层SQL统一的约束尽量减少裸SQL。裸SQL越多迁移时越痛苦。这个不是数据库的问题是工程规范的问题但每次迁移都验证一次这个结论。8.3 结合KCA备考的一种学习路径如果你在看KCA或者更高级别的国产数据库认证SQL语法差异大概率是考试重点之一。备考时不要只看教材里的语法对照表我建议直接自己搭一套MySQL和金仓的测试环境把常用的增删改查、聚合函数、窗口函数、存储过程各写一遍然后对比两边在同样条件下的输出。自己动手踩过的坑印象远比背表格深。我每次迁移收尾时还会做三件不起眼但很救命的检查把代码里的SQL全部扫一遍函数名、确认所有自增列都已经改成序列或IDENTITY、再用EXPLAIN核查一遍重点分页SQL的执行计划。做完这三件事项目上线基本就不会因为“SQL语法不兼容”这种低级问题翻车了。
返回列表