资讯动态

MySQL迁移PostgreSQL:六道必须跨越的语法鸿沟

发布时间:2026/10/10 14:53:49 来源:尧图企业网站定制
做数据库迁移的朋友这两年应该没少听说“MySQL 迁到 PostgreSQL”这个话题。原因无非那么几个PG 在标准 SQL 支持、JSON 处理、复杂查询、并发性能上确实有一手而且开源、无单库容量焦虑很多团队开始把它当备选甚至主力。但真正动手迁的时候很多人会发现一个扎心的事实MySQL 和 PG 虽然都是关系型数据库SQL 看着也差不多可真把语句拿过来跑报错能报到你怀疑人生。我帮团队做过几次从 MySQL 到 PG 的迁移说实话数据搬过去倒没那么难难就难在应用代码里那一堆 SQL 语法要跟着改。标题里“鸿沟”这个词用得很准——不是那种换个驱动就能填平的差距而是从数据类型、标识符规则、函数体系到 DML 写法处处都要小心。这篇文章我就把实际迁移中遇到频率最高、最容易让人卡壳的几道“鸿沟”整理出来每一条都会带上 MySQL 和 PG 的写法对照以及我踩过坑之后的处理方式。无论你是准备全量迁移还是只是想把新项目直接落在 PG 上这份清单都值得先过一遍。1. 第一道鸿沟DDL 与基础类型体系1.1 自增主键套路完全不一样如果你从 MySQL 迁过来最熟悉的建表方式应该是这样的-- MySQL CREATE TABLE user_info ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL DEFAULT COMMENT 名称, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;在 PG 里没有一个叫AUTO_INCREMENT的东西。最接近的替代方案有两个-- 方案一SERIAL 伪类型老派但常用 CREATE TABLE user_info ( id serial PRIMARY KEY, name varchar(50) NOT NULL DEFAULT ); -- 方案二IDENTITY 列SQL 标准PG 10 推荐 CREATE TABLE user_info ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name varchar(50) NOT NULL DEFAULT );我第一次迁移时就栽在这里。serial虽然写起来简单但它在底层会创建一个独立的序列对象pg_dump恢复数据时必须保证序列的当前值大于已有数据的最大值否则后面插入数据就会撞主键。用IDENTITY会更省心一些但要注意GENERATED ALWAYS AS IDENTITY默认情况下不允许你手动插入指定值如果业务里有“显式指定主键插入”的场景要么改成GENERATED BY DEFAULT AS IDENTITY要么在插入时加OVERRIDING SYSTEM VALUE。MySQL 的AUTO_INCREMENT还有一个隐含行为无论插入是否成功只要触碰到了自增列计数就会递增。PG 的序列默认CACHE 1每次取号是串行的在高并发批量插入下性能上反而没什么问题但行为上更“规矩”不会凭空跳号。1.2 基础类型映射表迁移前先对照一遍类型映射是很多人容易忽略的地方。比如 MySQL 里顺手就写的TINYINTPG 里压根没有这个类型建表直接报错。我把常用类型对照整理成一个表方便你迁移时对着改MySQL 类型PostgreSQL 类型说明TINYINTSMALLINT / BOOLEAN表示 0/1 时用 BOOLEAN表示整数用 SMALLINTSMALLINTSMALLINT直接对应MEDIUMINTINTEGERPG 没有 MEDIUMINT用 INT 代替INT / INTEGERINTEGER直接对应注意 PG 的 INT 就是 4 字节有符号BIGINTBIGINT直接对应UNSIGNED 属性无对应PG 没有无符号整数需要检查是否有负数场景否则用 BIGINT 扩大范围FLOAT / DOUBLEREAL / DOUBLE PRECISION逐个对应DECIMAL / NUMERICNUMERIC直接对应但要注意精度写法一致DATETIMETIMESTAMPMySQL 的 DATETIME 没时区PG 的 TIMESTAMP 也没时区对应即可TIMESTAMPTIMESTAMPTZ如果需要自动带时区建议 PG 侧用 TIMESTAMPTZDATEDATE直接对应CHAR / VARCHARCHAR / VARCHAR基本对应PG 不区分字符集TEXTTEXT直接对应BLOBBYTEA对应二进制大对象ENUM自定义类型或 CHECK 约束可以 CREATE TYPE AS ENUM但改枚举值麻烦我一般建议用 CHECKBOOLEANBOOLEANMySQL 的 BOOLEAN 是 TINYINT(1)PG 是真布尔类型PG 没有 MySQL 里的unsigned、zerofill这些列属性迁移时如果原表里有INT UNSIGNED最稳妥的做法是直接升级成BIGINT否则数据超过 21 亿之后会溢出线上炸了才反应过来就晚了。1.3 DDL 事务化与注释语法还有一个特别容易被忽略的点PG 的 DDL 支持事务回滚MySQL 的 DDL 会隐式提交。这意味着什么在 PG 里你可以这样写BEGIN; ALTER TABLE user_info ADD COLUMN age INT; ALTER TABLE user_info ADD COLUMN grade VARCHAR(10); -- 如果发现第二步写错了直接 ROLLBACK整个事务全部撤销 COMMIT;但在 MySQL 里ALTER TABLE一旦执行前面开启的事务就自动提交了想回滚根本没门。这个差异在需要批量修改线上表结构、而且操作有风险的时候对运维流程的影响非常大。我每次在 PG 上做结构变更都会顺手套一个显式事务先在测试环境跑一遍再上生产出错了也能干净回滚。另外 MySQL 建表时直接跟在列后面的COMMENT注释在 PG 里要写成独立语句COMMENT ON TABLE user_info IS 用户表; COMMENT ON COLUMN user_info.name IS 名称;这个改动虽然机械但要改的地方多容易漏。建议迁移时写个小脚本把information_schema里的列注释统一生成COMMENT ON语句。2. 第二道鸿沟标识符、字符串与排序2.1 大小写敏感性能把人整疯的第一个细节这可能是所有 MySQL 老手迁到 PG 之后遇到的第一块绊脚石。MySQL 在 Windows 和 macOS 上默认表名不区分大小写字段名无论怎么定义查询时大小写都能匹配上。PG 的行为完全不同不加引号的标识符会自动折叠成小写。举个例子在 MySQL 里建表CREATE TABLE UserInfo ( Name VARCHAR(50) );这条语句在 MySQL 里能正常用因为 MySQL 对字段名不敏感。但在 PG 里执行后表名实际变成了userinfo列名变成了name。你如果习惯性写SELECT Name FROM UserInfo;PG 会直接报column name does not exist。表面上看是报列不存在其实是因为 PG 把不带引号的Name转成了name去匹配恰恰能匹配上但UserInfo被转成了userinfo去匹配全小写的表名也能匹配上所以这个例子反而不报错。真正会报错的是反过来你用双引号建了大小写混合的表名或列名之后每次查询都必须带双引号否则 PG 又会折叠成小写去找于是必然报错。这个坑怎么避我给自己定了一条规矩迁移目标只要锁定 PG表名、字段名一律全小写下划线命名查询时不加引号。如果历史表里有驼峰或大写迁移时就统一做一次重命名省得后续每个 SQL 都提心吊胆。项目里如果用了 ORM也要检查是否默认给标识符加引号比如 Hibernate 在某些配置下会生成user_info这种带引号的 SQL这反而会让 PG 严格按引号内的名字匹配一旦前后大小写不一致就会出问题。2.2 字符串拼接与比较规则字符串也是重灾区。MySQL 里做字符串拼接大家最常用的是CONCATSELECT CONCAT(first_name, last_name) FROM user_info;PG 同样支持CONCAT但也支持更简洁的||SELECT first_name || last_name FROM user_info;看起来没什么问题真正的坑在 MySQL 里||默认是逻辑“或”不是拼接。如果你从 MySQL 迁过来的老代码里有WHERE a || b这种用法到了 PG 语义就完全变了轻则结果不对重则直接报类型错误。反过来如果代码里大量用CONCATPG 也能兼容只是要注意MySQL 的CONCAT遇到NULL参数时返回NULLPG 的||遇到NULL也会返回NULL这点两者倒是一致的。更隐蔽的是排序和比较规则。MySQL 默认的utf8mb4_general_ci排序规则对大小写不敏感所以WHERE name admin能匹配到Admin。PG 默认的排序规则继承库的LC_COLLATE通常是区分大小写的同样的查询会查不到数据。我迁移过一个后台管理系统上线之后有个登录接口怎么都验不过查了半天才发现是用户名大小写敏感导致的账号匹配失败。解决方案有两个要么改 SQL 用WHERE lower(name) lower(admin)要么建一个不区分大小写的索引或者干脆在 PG 里启用citext扩展CREATE EXTENSION citext; -- 然后把字段类型改成 CITEXT如果数据量不大我建议直接统一成CITEXT如果数据量很大改成lower()函数表达式索引更稳妥毕竟citext有点“黑魔法”性质性能和排序行为都可能和普通 text 有细微差异。2.3 NULL 排序默默影响分页结果的隐形坑排序规则里还有一个细节很容易被分页功能坑到NULL 值排在哪里。MySQL 中升序排序时 NULL 默认排在前面降序排序时 NULL 默认排在最后。PG 的默认行为恰好相反升序时 NULL 默认排在最后NULLS LAST降序时 NULL 默认排在前面NULLS FIRST。别小看这个差异。我之前遇到过一个列表页在迁移后“顺序不对”的反馈排查半天才发现不是业务逻辑改了纯粹是某个排序列里有 NULL两边的排序位置不一样导致整个列表的条目前后颠倒了。修复办法也简单排序时显式指定ORDER BY update_time DESC NULLS LAST; ORDER BY update_time ASC NULLS FIRST;任何对顺序敏感的分页查询迁移后都要检查一遍 NULL 排序。很多 ORM 不会帮你处理这个差异越到底层越明显。3. 第三道鸿沟日期时间函数3.1 取当前时间别把 MySQL 习惯带过去MySQL 里拿当前时间最常用的是NOW()、CURDATE()、CURRENT_TIMESTAMP。PG 里NOW()和CURRENT_TIMESTAMP也存在可以直接用但CURDATE()是没有的要写成CURRENT_DATE。同样MySQL 的CURTIME()在 PG 里对应CURRENT_TIME。这块其实还好真正要命的是函数返回类型带来的行为差异。MySQL 的NOW()返回的是DATETIME不携带时区PG 的NOW()返回的是timestamptz它的显示值会跟随会话的TimeZone参数变化。如果你的应用在 MySQL 里用DATETIME存“本地时间”迁到 PG 后如果用了TIMESTAMPTZ同样的NOW()在不同时区的客户端里查出来可能差几个小时。保险的做法是应用里不管时区PG 统一用TIMESTAMP无时区配合应用层传入的本地时间或者统一用TIMESTAMPTZ配合SET timezone管理。最怕的是两种类型混着用时间对不上都不知道是谁的锅。3.2 DATE_FORMAT 秒变 TO_CHAR这是出现频率最高的报错MySQL 里格式化日期写惯了DATE_FORMAT(created_at, %Y-%m-%d %H:%i:%s)的人到了 PG 都会撞一鼻子灰。PG 没有DATE_FORMAT对应的是TO_CHAR-- MySQL SELECT DATE_FORMAT(created_at, %Y-%m-%d %H:%i:%s) FROM orders; -- PostgreSQL SELECT TO_CHAR(created_at, YYYY-MM-DD HH24:MI:SS) FROM orders;占位符完全不是一套体系含义MySQLPostgreSQL四位年份%YYYYY两位年份%yYY月%mMM日%dDD小时24 小时制%HHH24分钟%iMI秒%sSS这种替换本身不复杂但代码里量大、写法多样很容易漏。我迁移时的做法是直接写一个正则把项目里所有DATE_FORMAT(...)匹配出来批量转成TO_CHAR(...)再人工 review。不要一个个手改效率低还容易看漏。3.3 日期运算DATEDIFF 没了INTERVAL 更强MySQL 里计算两个日期之间差多少天用DATEDIFFPG 里没有这个函数。最简单的替代是直接做减法-- MySQL SELECT DATEDIFF(2024-03-01, 2024-02-01); -- 返回 29 -- PostgreSQL SELECT DATE 2024-03-01 - DATE 2024-02-01; -- 返回 29类型是 INTEGER如果原本用DATEDIFF返回的是天数差那上面这种减法完全等价。但如果要算月数、年数MySQL 的TIMESTAMPDIFF对应 PG 要费点劲可以用EXTRACT(YEAR FROM age(...))或者EXTRACT(MONTH FROM age(...))但边界情况和 MySQL 的行为略有不同需要仔细验证。反过来PG 的INTERVAL类型真的很好用-- PG 写法直接在日期上加时间间隔 SELECT NOW() INTERVAL 1 day; SELECT NOW() INTERVAL 3 hours;MySQL 也有DATE_ADD和INTERVAL关键字但表达力和 PG 有一点区别。我迁移后的新代码更愿意在 PG 上直接写date interval 1 day语义一目了然。4. 第四道鸿沟DML 语法差异4.1 upsertON DUPLICATE KEY UPDATE 换成了 ON CONFLICT这是每条迁移清单里都绕不开的经典题。MySQL 写“有则更新无则插入”INSERT INTO user_info (id, name) VALUES (1, 张三) ON DUPLICATE KEY UPDATE name 张三;PG 的写法是INSERT INTO user_info (id, name) VALUES (1, 张三) ON CONFLICT (id) DO UPDATE SET name EXCLUDED.name;两者看着像但有三个关键差异第一PG 的ON CONFLICT必须明确指定冲突目标。如果表上有多个唯一约束你必须写清楚是针对哪个列、哪个约束冲突比如ON CONFLICT (id)、ON CONFLICT ON CONSTRAINT user_info_name_key。MySQL 的ON DUPLICATE KEY UPDATE不用管只要任何唯一键冲突都会触发更新这反而容易引发歧义。第二PG 在DO UPDATE子句里引用“本次待插入的值”要用EXCLUDED这个虚拟表。EXCLUDED.name表示当前这条语句未能插入进去的那一行里的 name。第三PG 还提供了DO NOTHING选项MySQL 没有直接对应INSERT INTO user_info (id, name) VALUES (1, 张三) ON CONFLICT (id) DO NOTHING;还有一个我在实战中踩过的坑PG 的ON CONFLICT DO UPDATE的WHERE子句里不能引用EXCLUDED之外的其他表也就是说不能像UPDATE ... FROM 其他表那样关联更新。如果你的 upsert 场景需要引用别的表数据就得改成先UPDATE再INSERT或者在应用层拼好值再写进去。4.2 UPDATE JOIN 与 DELETE JOINPG 里写不出来MySQL 支持非常直观的多表更新-- MySQL UPDATE orders o JOIN users u ON o.user_id u.id SET o.user_name u.name WHERE u.status 1;PG 不支持UPDATE ... JOIN但有一个更接近标准 SQL 的替代方案用FROM子句-- PostgreSQL UPDATE orders o SET user_name u.name FROM users u WHERE o.user_id u.id AND u.status 1;注意 PG 这种写法里SET子句中的o.user_name必须写成表名前缀吗其实可以只写SET user_name u.name但如果目标表在UPDATE语句中参与了WHERE条件最好还是加上前缀避免歧义。MySQL 风格的多表 DELETE 也是一样-- MySQL DELETE o FROM orders o JOIN users u ON o.user_id u.id WHERE u.status 0; -- PostgreSQL DELETE FROM orders o USING users u WHERE o.user_id u.id AND u.status 0;这个改动没有技术难度纯粹是语法习惯切换。但如果在迁移前没做全面扫描运行时就会在JOIN处直接语法报错。4.3 LIMIT 分页与 GROUP BY 的严格性MySQL 里写分页一种常见写法是LIMIT offset, count比如SELECT * FROM user_info LIMIT 100, 20;PG 不支持这种逗号写法必须写成SELECT * FROM user_info LIMIT 20 OFFSET 100;LIMIT count OFFSET offset这种写法两边都兼容所以迁移时优先把代码改成这种。不过更值得关注的是深分页性能。LIMIT 100000, 20这种写法MySQL 和 PG 都扛不住因为数据库都要先扫前 100000 行再丢掉。PG 上更推荐用“Keyset 分页”SELECT * FROM user_info WHERE id 100000 ORDER BY id LIMIT 20;这种写法走主键索引翻页再深也不怕。迁到 PG 后不改分页逻辑的话性能可能比 MySQL 还差改成分页游标形式才是真的把 PG 用对了。GROUP BY 的差异也值得单独说。MySQL 在默认sql_mode下SELECT里可以出现没有出现在GROUP BY中、也没有被聚合函数包住的列比如-- MySQL 默认可能不报错5.7 的 ONLY_FULL_GROUP_BY 下会报 SELECT id, name, MAX(score) FROM student GROUP BY class_id;PG 从第一天开始就严格遵守 SQL 标准SELECT列表里的每一列要么出现在GROUP BY中要么被聚合函数包裹否则直接报错。所以迁移时那些“钻 MySQL 空子”的 SQL必须逐条改写明确要保留哪一列。我一般是结合DISTINCT ON来实现“取分组内某行”的需求比如取每个班级里分数最高的那条记录SELECT DISTINCT ON (class_id) id, name, score FROM student ORDER BY class_id, score DESC;这个写法在 MySQL 里没有直接对应但迁到 PG 后真的很好用。4.4 RETURNINGPG 给人最爽的礼包MySQL 里插入一条数据后想拿自增主键要么用LAST_INSERT_ID()要么在 ORM 里通过驱动回填。PG 给你开了一个挂INSERT INTO user_info (name) VALUES (张三) RETURNING id;不仅INSERT可以UPDATE和DELETE也可以UPDATE user_info SET status 1 WHERE id 10 RETURNING id, name; DELETE FROM user_info WHERE id 10 RETURNING id;这个特性在迁移后非常值得推广。比如批量更新后要记录受影响行的完整信息以前要查一次才能拿到现在一条语句就搞定了。我在迁移后写新功能时基本都会利用RETURNING它能省掉不少多余的查询和网络往返。5. 第五道鸿沟JSON 与全文检索5.1 JSON 函数与操作符MySQL 从 5.7 开始支持 JSON 类型PG 从 9.2 就开始支持9.4 以后有了jsonb。两边基础能力都不弱但写法上差异明显。MySQL 里提取 JSON 字段常见写法SELECT JSON_EXTRACT(info, $.name) FROM user_info; SELECT info - $.name FROM user_info;PG 里要取 JSON 字段的值SELECT info - name FROM user_info; -- 返回 jsonb 类型 SELECT info - name FROM user_info; -- 返回 text 类型注意 MySQL 的-路径前面要带$PG 的-直接用键名。如果写成info - $.namePG 会当成一个叫$.name的字符串键去取取出来是 NULL查半天都查不出问题的那种。修改 JSON 字段MySQL 是JSON_SETUPDATE user_info SET info JSON_SET(info, $.age, 18) WHERE id 1;PG 是jsonb_setUPDATE user_info SET info jsonb_set(info, {age}, 18::jsonb) WHERE id 1;两边的路径写法也不同MySQL 用$.agePG 用{age}数组形式。如果是嵌套路径PG 里写作{a, b, c}MySQL 写$.a.b.c。还有类型选择的问题。MySQL 的 JSON 类型就是二进制存储没有json和jsonb之分PG 却有两种。我的建议很直接生产环境一律用jsonb它查询时无需重新解析支持 GIN 索引还支持包含、存在性判断等高级操作json类型只有在需要保留原始格式和键顺序时才值得考虑。5.2 全文检索从 MATCH...AGAINST 到 tsvectorMySQL 做全文检索最简单的方式是建FULLTEXT索引然后MATCH...AGAINSTALTER TABLE article ADD FULLTEXT INDEX ft_title (title, content); SELECT * FROM article WHERE MATCH(title, content) AGAINST (数据库 迁移);PG 的原生全文检索体系完全不一样核心概念是tsvector和tsquery。你需要先建一个向量列或者直接用表达式索引CREATE INDEX idx_article_fts ON article USING GIN (to_tsvector(english, title || || content)); SELECT * FROM article WHERE to_tsvector(english, title || || content) to_tsquery(english, 数据库 迁移);这里要注意PG 的全文检索是按语言的默认用english配置中文分词效果取决于自带的zhparser或pg_jieba扩展。如果迁移的业务原本就在 MySQL 里用中文全文检索想平移到 PG前置工作就是解决分词器问题。这件事复杂度不止“语法鸿沟”这么简单涉及到扩展安装和分词效果调优我建议在迁移规划阶段就单独列为一项别临时抱佛脚。如果只是做简单的关键字 LIKE 查询那就不必上全文索引PG 的ILIKE配合pg_trgm索引也够用。6. 第六道鸿沟函数、触发器与系统管理6.1 存储过程和触发器语法差异远超预期MySQL 的存储过程是CREATE PROCEDURE配DELIMITER写一堆语句。PG 更常用的其实是CREATE FUNCTION语言用plpgsql-- MySQL 风格简化 DELIMITER $$ CREATE PROCEDURE update_user_status(IN uid INT) BEGIN UPDATE user_info SET status 1 WHERE id uid; END$$ DELIMITER ;-- PostgreSQL 风格 CREATE OR REPLACE FUNCTION update_user_status(uid INT) RETURNS VOID AS $$ BEGIN UPDATE user_info SET status 1 WHERE id uid; END; $$ LANGUAGE plpgsql;这里不只是语法不同有几个点非常容易混淆第一PG 里函数必须写返回类型即使没有返回值也要写RETURNS VOID。第二PG 里的过程语言块用$$作定界符MySQL 用BEGIN...END结构两边逻辑控制流虽然都有 IF、WHILE、CASE但 PL/pgSQL 的变量声明、异常捕获、游标写法差异很大基本不能直接复制。第三触发器差异更大。MySQL 的触发器直接在CREATE TRIGGER ... FOR EACH ROW后面写逻辑。PG 的触发器必须先创建一个返回TRIGGER类型的函数再写CREATE TRIGGER ... EXECUTE FUNCTION 函数名()-- PG先建函数 CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- PG再建触发器 CREATE TRIGGER trg_user_updated_at BEFORE UPDATE ON user_info FOR EACH ROW EXECUTE FUNCTION set_updated_at();迁移时如果 MySQL 的存储过程里逻辑比较复杂我一般不会逐句翻译而是建议把逻辑挪到应用层或者用 PG 更地道的写法重写。逐行翻译 PL/pgSQL 的体验很糟维护成本也高。6.2 系统表与诊断命令SHOW 全家桶失效了MySQL 排查问题习惯了SHOW TABLES、SHOW CREATE TABLE、SHOW PROCESSLIST、SHOW VARIABLES。到了 PG这套基本失效。PG 的SHOW只能查配置参数比如SHOW timezone;而列表类信息要查系统视图。需求MySQL 命令PostgreSQL 替代查看所有表SHOW TABLES\dt或SELECT tablename FROM pg_tables WHERE schemanamepublic;查看建表语句SHOW CREATE TABLE t无直接命令用pg_dump --schema-only -t t查看查看当前连接/进程SHOW PROCESSLISTSELECT * FROM pg_stat_activity;查看配置变量SHOW VARIABLESSELECT name, setting FROM pg_settings;定位慢查询SHOW STATUS / 慢日志pg_stat_statements或EXPLAIN ANALYZE查看索引SHOW INDEX FROM t\d t或查询pg_indexes这些系统视图迁移后会成为新常态。尤其是pg_stat_activity它还提供了pg_cancel_backend(pid)和pg_terminate_backend(pid)两个函数来杀会话比 MySQL 里KILL的粒度更细。写监控脚本时我会优先用pg_stat_statements看 TOP SQL这在 MySQL 里通常要装插件或开慢日志才能做到。6.3 迁移实操别拿 mysqldump 硬灌用 pgloader最后说说迁移工具链。有人图省事直接把 MySQL 的mysqldump结果改两个引号就往 PG 里灌这是最要命的做法。MySQL 的 dump 文件里全是ENGINEInnoDB、反引号、AUTO_INCREMENT这些 PG 不认识的语法灌进去就是一堆报错。我推荐用pgloader它是专门干 MySQL 到 PG 迁移的工具能自动处理大部分类型映射。基本用法很简单pgloader mysql://user:passwordlocalhost/mydb postgresql://user:passwordlocalhost/mydb更复杂的场景写一个.load文件控制表映射、类型转换规则、索引迁移方式。pgloader 会把大部分常用类型转掉但它不是万能的比如 MySQL 的ENUM、SET类型它转出来可能不完全符合业务预期需要手工修正。我自己迁移时会先把应用代码里所有的 SQL 文本收集起来做一次静态扫描提前找出LIMIT 逗号、DATE_FORMAT、ON DUPLICATE KEY UPDATE、UPDATE JOIN这些高危写法。然后建一个影子 PG 库把 MySQL 线上数据通过 pgloader 导过去再把应用连到影子库跑回归。这个过程最花时间的不是数据搬移而是业务 SQL 的逐条适配。写在迁移之后的一些体会几轮 MySQL 到 PG 的迁移做下来我的体会是语法差异不复杂但极其琐碎。它不像换一种编程语言那样要重新学思想更像是同一种语言的两个方言你明明能看懂但一说就被对方纠正。最有效的准备不是临时百度函数对照表而是提前把项目里的 SQL 全部盘点一遍。我自己的习惯是迁移前把 MySQL 慢查询日志里出现过的、应用代码里写死的、ORM 生成的三层 SQL 全部收集起来在 PG 上批量跑一遍。这一步能提前暴露九成的语法不兼容问题而且比上线后再回头改要省心得多。最后再分享一个实操判断如果一个团队对 PG 并不熟悉我建议新项目直接落在 PG 上一边写一边踩坑存量 MySQL 项目如果业务稳定、查询简单就没有必要强行迁移迁移的成本大概率比你想象的高。如果决定要迁那就给自己留足至少两轮测试迭代的时间第一轮解决语法第二轮解决行为和性能差异。毕竟“能跑起来”和“跑得和原来一样稳”中间还隔着很长一段距离。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑