MySQL 迁移 PostgreSQL 常见问题与避坑指南
MySQL 迁移 PostgreSQL,真正麻烦的地方通常不是 CRUD 语法。
而是迁移之后发现:
连接突然满了 |
所以这篇文章不从数据库理论开始,而是直接按线上常见问题横向对比。
一、先看总表
| 问题 | MySQL | PostgreSQL | 迁移时最容易踩的坑 |
|---|---|---|---|
| 连接数 | 可以相对较高 | 单连接成本更高 | 直接照搬 MySQL 连接池 |
| 连接超时 | connect_timeout | 客户端 connect timeout | 把连接超时误认为 SQL 慢 |
| SQL执行超时 | max_execution_time | statement_timeout | PG 默认策略不同 |
| 锁等待 | innodb_lock_wait_timeout | lock_timeout | SQL 本身不慢,是在等锁 |
| 空闲连接 | wait_timeout | idle_session_timeout | 连接池拿到失效连接 |
| 空闲事务 | 相对不突出 | idle in transaction 非常危险 | 长事务阻塞 VACUUM |
| 断线 | server has gone away | server closed connection | 没设计连接池重连 |
| 类型转换 | 比较宽松 | 非常严格 | varchar = bigint 直接报错 |
| 事务错误 | 某些错误后还能继续 | 一条失败,事务进入 aborted | catch 后继续执行 |
| 死锁 | InnoDB 检测 | PG 检测 | 只调大 timeout |
| UPSERT | ON DUPLICATE KEY | ON CONFLICT | SQL 无法直接兼容 |
| 自增 | AUTO_INCREMENT | Sequence / Identity | 导数据后 Sequence 没同步 |
| Boolean | tinyint(1) | boolean | 0/1 字段迁移错误 |
| JSON | JSON | JSONB 更常用 | 机械迁移成 JSON |
| 分页 | LIMIT offset,size | LIMIT size OFFSET offset | SQL 方言不同 |
| 索引 | B-Tree 为主 | BTree / GIN / GiST / BRIN | 索引照搬但执行计划变了 |
| UPDATE | InnoDB Undo | 新 Tuple Version | 高频更新产生 Dead Tuple |
| 垃圾回收 | Purge | VACUUM | 不理解 VACUUM |
| 表膨胀 | 有但感知较弱 | 更需要关注 | UPDATE/DELETE 后磁盘暴涨 |
| 执行计划 | EXPLAIN | EXPLAIN ANALYZE 很强 | 只看有没有索引,不看实际执行 |
二、问题 1:连接数突然打满
MySQL
以前可能是:
10 个服务 |
MySQL:
max_connections = 1500 |
还能继续跑。
PostgreSQL
如果直接迁移:
10 个服务 |
就可能出现:
too many clients already |
或者数据库 CPU / 内存明显上升。
原因之一是 PostgreSQL 的连接模型与 MySQL 不完全相同。
错误迁移方式
MySQL |
这不是解决方案。
更合理
Application |
例如 Go:
db.SetMaxOpenConns(50) |
核心区别
MySQL 思路: |
三、问题 2:SQL Timeout,但 SQL 明明有索引
例如:
UPDATE account |
正常执行:
0.3 ms |
线上却:
30 seconds timeout |
很多人的第一反应:
SQL 慢 |
但真正情况可能是:
Transaction A |
实际:
SQL执行:0.3ms |
所以:
Timeout ≠ SQL慢。
四、MySQL / PostgreSQL Timeout 对照
一次数据库请求 |
对应:
| 类型 | MySQL | PostgreSQL |
|---|---|---|
| 建立连接 | connect timeout | connect timeout |
| SQL执行 | max_execution_time | statement_timeout |
| 锁等待 | innodb_lock_wait_timeout | lock_timeout |
| 空闲连接 | wait_timeout | idle_session_timeout |
| 空闲事务 | 无完全对应 | idle_in_transaction_session_timeout |
排查 Timeout 时不要直接:
Timeout |
应该:
Timeout |
五、问题 3:断线重连
MySQL 常见
MySQL server has gone away |
Lost connection to MySQL server |
PostgreSQL 常见
server closed the connection unexpectedly |
connection reset by peer |
broken pipe |
两边本质类似:
连接建立 |
正确方式:
Connection Pool |
不要在业务代码里永久保存:
conn := db.Conn(...) |
然后无限使用。
六、问题 4:同一条 SQL,MySQL 能跑,PostgreSQL 报错
这是迁移时最直观的问题。
MySQL:
SELECT * |
假设:
id = BIGINT |
MySQL 很多情况下会自动转换:
'10001' |
然后执行。
PostgreSQL 更严格。
可能出现:
operator does not exist: |
也就是:
BIGINT |
七、最危险的“临时修复”:CAST
开发看到报错以后,很容易:
WHERE user_id::text = '10001' |
SQL 可以跑了。
但是问题来了。
原来:
user_id |
改成:
user_id::text |
正确方式应该是:
WHERE user_id = 10001 |
即:
错误做法: |
这点在 Go 项目里尤其重要。
八、问题 5:事务里一个 SQL 报错,后面全部执行不了
这是 PostgreSQL 和 MySQL 使用体验差异中非常重要的一点。
例如:
BEGIN; |
发生唯一键冲突:
duplicate key value violates unique constraint |
代码觉得:
没事 |
然后:
UPDATE users |
PostgreSQL:
current transaction is aborted, |
实际事务状态:
BEGIN |
所以 PostgreSQL:
事务 SQL 报错 |
如果业务确实需要局部失败:
SAVEPOINT |
九、问题 6:死锁
MySQL:
Deadlock found when trying to get lock |
PostgreSQL:
deadlock detected |
典型:
Transaction A Transaction B |
错误解决方式:
Deadlock |
没有意义。
因为:
A 等 B |
等 30 秒还是死锁。
正确解决:
统一加锁顺序 |
例如:
永远按照: |
所以两个事务都:
1001 |
同时应用层:
Deadlock |
十、问题 7:索引明明存在,为什么 PostgreSQL 不走?
例如:
CREATE INDEX idx_orders_user_id |
SQL:
SELECT * |
可能:
Index Scan |
但改成:
WHERE user_id::text = '10001' |
可能:
Seq Scan |
另外:
WHERE DATE(created_at) = '2026-08-13' |
也可能影响索引。
建议:
WHERE created_at >= '2026-08-13 00:00:00' |
十一、不要认为 Seq Scan 一定不好
例如:
orders |
这种情况下:
Index Scan |
不一定比:
Seq Scan |
快。
所以迁移 PostgreSQL 后不要:
看到 Seq Scan |
而应该:
EXPLAIN ANALYZE |
十二、问题 8:MySQL 的联合索引照搬后,PG 为什么性能不一样?
MySQL 原来:
INDEX idx_user_status_time( |
SQL:
SELECT * |
MySQL:
运行很好 |
迁移 PostgreSQL 后:
SQL能跑 |
不代表:
执行计划一样 |
因为:
MySQL Optimizer |
所以迁移应该:
旧 MySQL SQL |
不是:
DDL复制完成 |
十三、问题 9:UPDATE 越多,PostgreSQL 表为什么越来越大?
这是 PostgreSQL 和 MySQL 非常核心的差异。
MySQL InnoDB 可以简单理解:
Current Row |
PostgreSQL:
Tuple V1 |
再次 UPDATE:
Tuple V1 → Dead |
所以:
UPDATE |
可能产生大量:
Dead Tuple |
十四、这就是为什么 PostgreSQL 必须理解 VACUUM
PostgreSQL:
UPDATE / DELETE |
如果 Autovacuum 跟不上:
大量 UPDATE |
例如:
有效业务数据: |
这种问题在 PostgreSQL 中并不罕见。
十五、哪些业务最容易出现 Bloat?
最典型:
订单表 |
一个订单不断:
UPDATE status |
以及:
资产 |
都是高频 UPDATE 场景。
所以交易系统迁移 PostgreSQL 时:
Autovacuum 不是 DBA 才需要知道的东西,核心开发也必须理解。
十六、问题 10:一个没提交的事务为什么能把数据库拖慢?
例如:
BEGIN; |
执行一些 SQL。
然后程序因为 Bug:
忘记 Commit |
连接变成:
idle in transaction |
PG 中:
Long Transaction |
所以 MySQL 迁移 PostgreSQL 后:
连接监控 |
不能只看:
active connection |
还要重点看:
idle in transaction |
十七、问题 11:导完数据后,自增 ID 冲突
MySQL:
AUTO_INCREMENT |
比如:
当前最大 ID: |
下一条:
100001 |
PostgreSQL 常用:
Identity / Sequence |
迁移数据时如果只是:
COPY data |
导入:
1 |
但是 Sequence 仍然:
1 |
下一条 INSERT:
id = 1 |
然后:
duplicate key |
所以迁移完成必须验证:
MAX(id) |
必须保证:
Sequence >= MAX(id) |
十八、问题 12:UPSERT 直接不能用了
MySQL:
INSERT INTO balance(user_id, amount) |
PostgreSQL:
INSERT INTO balance(user_id, amount) |
迁移项目中可以直接全局搜索:
ON DUPLICATE KEY UPDATE |
全部需要处理。
十九、问题 13:LIMIT 写法不同
MySQL:
LIMIT 100, 20 |
意思:
OFFSET = 100 |
PostgreSQL:
LIMIT 20 OFFSET 100 |
所以:
MySQL |
二十、问题 14:大 OFFSET 两边都会慢
例如:
SELECT * |
数据库实际上:
读取前面大量数据 |
更好的方式:
SELECT * |
也就是:
OFFSET Pagination |
对于:
订单 |
尤其重要。
二十一、问题 15:MySQL 的 tinyint(1) 到 PG 怎么处理?
MySQL:
is_deleted TINYINT(1) |
常见:
0 = false |
PostgreSQL:
is_deleted BOOLEAN |
直接:
TRUE |
但不要把所有:
tinyint |
都改成:
boolean |
例如:
status = 0 Pending |
这种应该:
SMALLINT |
而不是 BOOLEAN。
二十二、问题 16:JSON 应该迁成 JSON 还是 JSONB?
MySQL:
JSON |
PostgreSQL:
JSON |
如果 JSON 只是:
保存 |
JSON 可以。
但如果需要:
WHERE 查询 |
通常应该重点考虑:
JSONB |
例如:
SELECT * |
再结合:
GIN Index |
所以:
MySQL JSON |
不要机械:
↓ |
应该:
MySQL JSON |
二十三、问题 17:时间字段最容易出现“数据没错,但业务错了”
MySQL 常见:
DATETIME |
PostgreSQL:
timestamp |
迁移时必须确认:
Application |
否则可能出现:
数据库: |
结果差:
8 Hours |
交易系统建议:
数据库内部 |
展示:
UTC |
二十四、问题 18:Go 代码不能只换 Driver
MySQL:
db.Query( |
PostgreSQL:
db.Query( |
也就是:
MySQL |
如果项目有几千条手写 SQL:
Driver 替换 |
只是整个迁移的一小部分。
二十五、数据库错误码也要重新改
MySQL:
1062 |
代表:
Duplicate Entry |
业务可能:
if mysqlErr.Number == 1062 { |
PostgreSQL:
23505 |
代表:
unique_violation |
常见 PG SQLSTATE:
| 场景 | PostgreSQL |
|---|---|
| 唯一键冲突 | 23505 |
| 外键冲突 | 23503 |
| 非空约束 | 23502 |
| 死锁 | 40P01 |
| 序列化失败 | 40001 |
最好从:
业务代码 |
改成:
DB Error Adapter |
这样以后数据库迁移会轻松很多。
二十六、MySQL → PostgreSQL 最常见错误迁移方式
最危险的是这种:
第一步: |
看起来完成了。
实际上应该:
Schema |
二十七、迁移过程中重点排查表
| 现象 | MySQL经验判断 | PostgreSQL 应重点检查 |
|---|---|---|
| SQL突然30秒 | 慢SQL | Lock / statement timeout |
| DB连接满 | max_connections | Pool / Long TX / PgBouncer |
| SQL报类型错误 | 很少遇到 | bigint / varchar / uuid 类型 |
| SQL有索引还慢 | 索引问题 | EXPLAIN ANALYZE |
| UPDATE越来越慢 | 锁/索引 | Dead Tuple / Bloat / Vacuum |
| DB磁盘突然变大 | 数据增加 | Bloat |
| 连接不释放 | Pool问题 | idle in transaction |
| SQL失败后后续全失败 | 奇怪 | Transaction Aborted |
| 主键突然重复 | AUTO_INCREMENT | Sequence 未同步 |
| JSON查询慢 | JSON索引 | JSONB + GIN |
| 大分页慢 | LIMIT优化 | Keyset Pagination |
| CPU突然升高 | SQL问题 | 连接数 + Seq Scan + Vacuum |
| 偶发死锁 | 重试 | Lock Order + Retry |
| 重启后大量报错 | 连接失效 | Pool健康检查 / 重连 |
二十八、生产排障思路也应该改变
以前 MySQL:
数据库慢 |
迁移 PostgreSQL 后:
数据库慢 |
二十九、最后浓缩成一句话
MySQL 迁移 PostgreSQL:
不是 |
真正最容易踩坑的可以归纳成:
① 类型更严格 |
如果原系统属于:
交易所 |
那么还需要把下面几项提高到最高优先级:
NUMERIC 精度 |
这些才是 MySQL → PostgreSQL 迁移完成后,最容易在线上真正出问题的地方。



