从MySQL迁移到PostgreSQL:那些没人告诉你的血泪避坑指南

2026-06-28 11 0

从MySQL迁移到PostgreSQL:那些没人告诉你的血泪避坑指南

说出来你可能不信,我们团队去年做了一个"疯狂"的决定:把公司的核心数据库从MySQL切换到PostgreSQL。

老板问:为什么要换?

我们答:为了更好的性能、更强大的SQL支持、以及那个听起来很厉害的JSONB。

老板说:行,换吧。

然后我们就踏上了为期三个月的踩坑之旅。今天把这个过程中踩得最惨烈的几个坑分享出来,希望后来者能少流点血——虽然我觉得该踩的坑迟早会踩。


坑一:varchar(255)的诅咒

你知道MySQL里varchar(255)意味着什么吗?在MySQL 5.7及之前,这个255是字符数,不是字节数。所以你存一个emoji进去,数据库愉快地接受了。

切换到PostgreSQL后,我们系统里所有用户的简介、评论、签名档全部炸了。因为PostgreSQL的varchar(n)里的n是字节数,而且它对字符集非常——怎么形容呢——较真。

-- MySQL: 存得下
INSERT INTO users (bio) VALUES ('Hello 🦞');

-- PostgreSQL: 不好意思,超长了
ERROR: value too long for type character varying(255)

你以为这是个例?天真。太长是你的错,不是emoji的错。PostgreSQL的哲学是:明确你的数据要多大,就告诉我多大,别让我猜。

所以我们花了两个通宵,把所有varchar(255)改成了varchar(1024)或text,谁让产品经理当年非要给用户开放"写小作文"的权限呢。

血的教训:迁移前一定要跑一遍数据长度扫描,emoji、emoji+中文、分词符串,这三个东西组合起来能让你对"字符数"产生全新的认识。


坑二:自增ID的"隐形杀手"

MySQL里你写AUTO_INCREMENT,用得飞起。PostgreSQL里对应的叫SERIAL,用法几乎一样。但这里有个坑,坑了我们整整两周才定位到。

看看这个查询:

-- MySQL
SELECT COUNT(*) FROM orders WHERE user_id = 12345;

-- PostgreSQL
SELECT COUNT(*) FROM orders WHERE user_id = 12345;

看起来一样对吧?执行计划完全不一样!

MySQL的AUTO_INCREMENT是一个暴露的计数器,查询优化器能精确知道最大ID是多少。PostgreSQL的SERIAL是一个隐藏的序列对象,优化器只能靠统计信息估算。

结果就是:当你的user_id字段分布不均匀时,MySQL能快速跳过不需要扫描的页面,PostgreSQL可能会老老实实地扫全表。

我们有个报表查询,在MySQL上跑0.3秒,PostgreSQL上跑40秒。explain analyze一看,优化器选了一个你以为很聪明但实际很傻的执行计划。

血的教训:迁移后务必用EXPLAIN ANALYZE重新审查所有核心查询,别相信"语法一样执行计划就一样"这种鬼话。


坑三:大小写敏感性的"文艺复兴"

MySQL在Linux上的默认设置是大小写敏感,Windows上是大小写不敏感。PostgreSQL则是全平台大小写敏感。

如果你在Windows上开发,MySQL的表名可能是这样:

SELECT * FROM User_Orders  -- 成功
SELECT * FROM user_orders  -- 也成功

到了PostgreSQL,对不起:

SELECT * FROM User_Orders  -- 成功
SELECT * FROM user_orders  -- ERROR: relation "user_orders" does not exist

我们有个历史遗留项目,表名大小写混用,上线第一天就收获了三十多个"表不存在"错误。

解决方案有两个:要么统一改成小写,要么在PostgreSQL配置文件里把lc_collate设成和MySQL Windows版本一样的规则。但说实话,配置文件改完了又可能引发其他奇怪问题。

血的教训:迁移前先检查所有SQL文件,统一命名规范。最好的办法是把表名全改成小写,这辈子都别再用驼峰命名法给PostgreSQL的表命名。


坑四:那个你不知道的"间隙锁"玩法

MySQL的InnoDB和PostgreSQL的MVCC,看起来都是并发控制机制,实际上是两个世界的生物。

MySQL用锁 + MVCC混合模式,InnoDB的行锁可以锁住不存在的记录(也就是间隙锁)。这个特性让我们实现某些业务逻辑时特别顺手。

PostgreSQL是纯MVCC,写不阻塞读,读不阻塞写,听起来很美好。但当你需要"只有一个人能抢到"这种业务逻辑时,PostgreSQL会给你一个经典的PG特有的错误:

ERROR: could not serialize access due to concurrent update

我们有个"限时抢购"功能,MySQL上跑了三年稳如老狗。PostgreSQL上第一次压测就出现了大量serialization failure。

解决方案是重写锁逻辑,用SELECT ... FOR UPDATE加上重试机制:

CREATE OR REPLACE FUNCTION grab_seat(p_seat_id INT)
RETURNS BOOLEAN AS $$
DECLARE
    v_available BOOLEAN;
BEGIN
    LOOP
        SELECT available INTO v_available
        FROM seats
        WHERE id = p_seat_id
        FOR UPDATE NOWAIT;
        
        IF NOT v_available THEN
            RETURN FALSE;
        END IF;
        
        UPDATE seats SET available = FALSE WHERE id = p_seat_id;
        IF FOUND THEN
            RETURN TRUE;
        END IF;
        -- 如果UPDATE没影响行,说明被抢了,重试
    END LOOP;
END;
$$ LANGUAGE plpgsql;

这段代码看起来有点变态,但它是PostgreSQL下正确处理"抢"这个动作的方式。

血的教训:所有依赖特定锁行为的代码,在迁移前都要review一遍。MySQL的某些"特性"其实是"技巧",换个数据库就失效了。


坑五:JSONB不是万能药

我们迁移之前就听说PostgreSQL的JSONB多么多么厉害,可以存灵活的半结构化数据,比MySQL的JSON强十倍。

于是兴冲冲地把一些原本存在text字段里的配置信息迁移到了JSONB。然后查询性能让我们怀疑人生。

-- 我们的配置存储
{"theme": "dark", "lang": "zh-CN", "notifications": {"email": true, "sms": false}}

-- 查询所有开启了email通知的用户
SELECT * FROM users 
WHERE config -> 'notifications' ->> 'email' = 'true';

这个查询在PostgreSQL里用不到任何索引,每次都要扫描全表。因为JSONB的查询语法太灵活了,优化器表示"我猜不透你的意图"。

后来我们老老实实地建了表达式索引:

CREATE INDEX idx_users_email_notif 
ON users ((config -> 'notifications' ->> 'email'));

但问题是,如果你有几十种不同的配置字段,每个都建索引,那还不如回到关系型设计。

血的教训:JSONB适合存储真正的"文档"数据,比如用户画像、日志等需要灵活字段的场景。如果你只是懒得建表,宁可别用JSONB,那只会让事情变得更糟。


总结:为什么我们还是换了?

说了这么多坑,你可能想问:既然这么坑,为什么还要换?

因为三个月后,我们的核心报表查询从平均800毫秒降到了50毫秒。我们有了强大的窗口函数、CTE、强大的全文搜索、以及那个虽然坑很多但确实好用的JSONB。

MySQL像是给你一把瑞士军刀,什么都能干,但每样都不是最顶尖。PostgreSQL像是给你一整套专业工具箱,用好了效率翻倍,用不好砸自己脚。

关键不是"哪个更好",而是"哪个更适合你的场景"。

如果你的业务是:大量简单查询、读写比例极高、表结构相对稳定——MySQL依然是王者。

如果你的业务是:复杂查询、报表分析、JSON数据处理、或者你需要PostgreSQL那些高级特性——那就做好踩坑的准备,坑踩完了就是坦途。

最后送大家一句话:没有银弹,只有银勺。数据库选型这事,适合比先进重要。

祝各位的数据库迁移之旅,少流血,多长肉。

相关文章

从笨拙到默契:我与 OpenClaw 的相爱相杀
RESTful API 设计路上踩过的那些坑,今天全部交代
SQL写得丑,数据库背锅:七个让查询变慢的作死操作
OpenClaw 使用经验分享:这只小龙虾是如何炼成的
OpenClaw 使用经验分享:这只小龙虾是如何炼成的
连接池泄漏的锅,代码居然不背——直到服务器冒烟那天

发布评论