MySQL 迁移 PostgreSQL 常见问题与避坑指南
MySQL 迁移 PostgreSQL,真正麻烦的地方通常不是 CRUD 语法。
而是迁移之后发现:
连接突然满了
SQL 超时了
索引不走了
事务卡死了
程序断线了
自增 ID 冲突了
同样 SQL 跑得比 MySQL 慢
UPDATE 越跑表越大所以这篇文章不从数据库理论开始,而是直接按线上常见问题横向对比。
一、先看总表
| 问题 | 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 个服务
每个服务 100 个连接
= 1000 ConnectionsMySQL:
max_connections = 1500还能继续跑。
PostgreSQL
如果直接迁移:
10 个服务
×
100 Connections
=
1000 PostgreSQL Connections就可能出现:
too many clients already或者数据库 CPU / 内存明显上升。
原因之一是 PostgreSQL 的连接模型与 MySQL 不完全相同。
错误迁移方式
MySQL
max_connections = 2000
↓
PostgreSQL
max_connections = 2000这不是解决方案。
更合理
Application
│
↓
Go Connection Pool
│
↓
PgBouncer
│
↓
PostgreSQL例如 Go:
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(time.Hour)
db.SetConnMaxIdleTime(10 * time.Minute)核心区别
MySQL 思路:
连接不够
↓
max_connections 调大
PostgreSQL 思路:
连接不够
↓
先检查连接池
↓
检查服务实例数量
↓
检查慢 SQL / 长事务
↓
必要时 PgBouncer三、问题 2:SQL Timeout,但 SQL 明明有索引
例如:
UPDATE account
SET balance = balance - 100
WHERE user_id = 10001;正常执行:
0.3 ms线上却:
30 seconds timeout很多人的第一反应:
SQL 慢
↓
是不是没索引?但真正情况可能是:
Transaction A
UPDATE account
WHERE user_id = 10001;
│
↓
持有 Row Lock
│
│
▼
Transaction B
UPDATE account
WHERE user_id = 10001;
│
↓
等待 30 秒
│
↓
Timeout实际:
SQL执行:0.3ms
等待锁:29999ms所以:
Timeout ≠ SQL慢。
四、MySQL / PostgreSQL Timeout 对照
一次数据库请求
│
▼
获取连接池
│
Pool Wait Timeout
│
▼
建立连接
│
Connect Timeout
│
▼
执行 SQL
/ \
/ \
Lock Timeout Statement 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
│
├─ 没拿到连接?
│
├─ 建连接失败?
│
├─ 在等锁?
│
├─ SQL 真慢?
│
└─ 网络问题?五、问题 3:断线重连
MySQL 常见
MySQL server has gone awayLost connection to MySQL serverPostgreSQL 常见
server closed the connection unexpectedlyconnection reset by peerbroken pipe两边本质类似:
连接建立
│
↓
放入连接池
│
↓
长时间未使用
│
↓
DB / Proxy / Network
将连接断开
│
↓
Application 再次拿到
│
↓
发现连接已失效正确方式:
Connection Pool
├─ Connection A √
├─ Connection B √
├─ Connection C ×
│ │
│ ↓
│ Destroy
│ │
│ ↓
│ New Connection
│
└─ Connection D √不要在业务代码里永久保存:
conn := db.Conn(...)然后无限使用。
六、问题 4:同一条 SQL,MySQL 能跑,PostgreSQL 报错
这是迁移时最直观的问题。
MySQL:
SELECT *
FROM users
WHERE id = '10001';假设:
id = BIGINTMySQL 很多情况下会自动转换:
'10001'
↓
10001然后执行。
PostgreSQL 更严格。
可能出现:
operator does not exist:
bigint = character varying也就是:
BIGINT
=
VARCHAR
×七、最危险的“临时修复”:CAST
开发看到报错以后,很容易:
WHERE user_id::text = '10001'SQL 可以跑了。
但是问题来了。
原来:
user_id
│
↓
BTree Index
│
↓
Index Scan改成:
user_id::text
│
↓
Function / Cast
│
↓
可能无法使用原索引
│
↓
Seq Scan正确方式应该是:
WHERE user_id = 10001即:
错误做法:
Column → CAST
正确做法:
Parameter → 正确的数据类型这点在 Go 项目里尤其重要。
八、问题 5:事务里一个 SQL 报错,后面全部执行不了
这是 PostgreSQL 和 MySQL 使用体验差异中非常重要的一点。
例如:
BEGIN;
INSERT INTO users(id)
VALUES(1);发生唯一键冲突:
duplicate key value violates unique constraint代码觉得:
没事
catch(error)
继续然后:
UPDATE users
SET name = 'karp'
WHERE id = 1;PostgreSQL:
current transaction is aborted,
commands ignored until end of transaction block实际事务状态:
BEGIN
│
▼
SQL 1
│
▼
ERROR
│
▼
Transaction = ABORTED
│
├──── SQL 2 ×
├──── SQL 3 ×
├──── SQL 4 ×
│
▼
ROLLBACK所以 PostgreSQL:
事务 SQL 报错
↓
不要继续
↓
ROLLBACK如果业务确实需要局部失败:
SAVEPOINT九、问题 6:死锁
MySQL:
Deadlock found when trying to get lockPostgreSQL:
deadlock detected典型:
Transaction A Transaction B
Lock User 1001 Lock User 1002
│ │
▼ ▼
Lock User 1002 Lock User 1001
│ │
└─────────┐ ┌─────────┘
▼ ▼
Deadlock错误解决方式:
Deadlock
↓
把 Timeout 从 5 秒改成 30 秒没有意义。
因为:
A 等 B
B 等 A等 30 秒还是死锁。
正确解决:
统一加锁顺序例如:
永远按照:
user_id ASC所以两个事务都:
1001
↓
1002
↓
1003同时应用层:
Deadlock
│
↓
Rollback
│
↓
Retry十、问题 7:索引明明存在,为什么 PostgreSQL 不走?
例如:
CREATE INDEX idx_orders_user_id
ON orders(user_id);SQL:
SELECT *
FROM orders
WHERE user_id = 10001;可能:
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'
AND created_at < '2026-08-14 00:00:00'十一、不要认为 Seq Scan 一定不好
例如:
orders
总记录数:1000万
查询结果:800万这种情况下:
Index Scan不一定比:
Seq Scan快。
所以迁移 PostgreSQL 后不要:
看到 Seq Scan
↓
强制加索引而应该:
EXPLAIN ANALYZE
│
├── actual time
├── rows
├── loops
├── cost
└── Scan Type十二、问题 8:MySQL 的联合索引照搬后,PG 为什么性能不一样?
MySQL 原来:
INDEX idx_user_status_time(
user_id,
status,
created_at
)SQL:
SELECT *
FROM orders
WHERE user_id = 10001
AND status = 1
ORDER BY created_at DESC
LIMIT 20;MySQL:
运行很好迁移 PostgreSQL 后:
SQL能跑不代表:
执行计划一样因为:
MySQL Optimizer
≠
PostgreSQL Optimizer所以迁移应该:
旧 MySQL SQL
│
↓
EXPLAIN
│
↓
记录计划
PG SQL
│
↓
EXPLAIN ANALYZE
│
↓
重新验证不是:
DDL复制完成
=
索引迁移完成十三、问题 9:UPDATE 越多,PostgreSQL 表为什么越来越大?
这是 PostgreSQL 和 MySQL 非常核心的差异。
MySQL InnoDB 可以简单理解:
Current Row
│
↓
Undo Log
│
↓
Old VersionPostgreSQL:
Tuple V1
UPDATE
│
▼
Tuple V1 → Old / Dead
Tuple V2 → New再次 UPDATE:
Tuple V1 → Dead
Tuple V2 → Dead
Tuple V3 → Current所以:
UPDATE
UPDATE
UPDATE
UPDATE
UPDATE可能产生大量:
Dead Tuple十四、这就是为什么 PostgreSQL 必须理解 VACUUM
PostgreSQL:
UPDATE / DELETE
│
▼
Dead Tuple
│
▼
Autovacuum
│
▼
清理 / 标记可复用空间如果 Autovacuum 跟不上:
大量 UPDATE
│
▼
大量 Dead Tuple
│
▼
Table Bloat
│
▼
表越来越大
│
▼
扫描越来越慢
│
▼
IO越来越高例如:
有效业务数据:
10 GB
实际表空间:
50 GB这种问题在 PostgreSQL 中并不罕见。
十五、哪些业务最容易出现 Bloat?
最典型:
订单表
│
├─ pending
├─ matching
├─ filled
├─ canceled
└─ settled一个订单不断:
UPDATE status
UPDATE filled_qty
UPDATE avg_price
UPDATE fee
UPDATE time以及:
资产
持仓
任务状态
队列任务
风控状态都是高频 UPDATE 场景。
所以交易系统迁移 PostgreSQL 时:
Autovacuum 不是 DBA 才需要知道的东西,核心开发也必须理解。
十六、问题 10:一个没提交的事务为什么能把数据库拖慢?
例如:
BEGIN;执行一些 SQL。
然后程序因为 Bug:
忘记 Commit
忘记 Rollback连接变成:
idle in transactionPG 中:
Long Transaction
│
▼
Old Snapshot
│
▼
旧 Tuple 不能正常回收
│
▼
Vacuum 受影响
│
▼
Dead Tuple 增长
│
▼
Table Bloat所以 MySQL 迁移 PostgreSQL 后:
连接监控不能只看:
active connection还要重点看:
idle in transaction十七、问题 11:导完数据后,自增 ID 冲突
MySQL:
AUTO_INCREMENT比如:
当前最大 ID:
100000下一条:
100001PostgreSQL 常用:
Identity / Sequence迁移数据时如果只是:
COPY data导入:
1
2
3
...
100000但是 Sequence 仍然:
1下一条 INSERT:
id = 1然后:
duplicate key所以迁移完成必须验证:
MAX(id)
VS
Sequence Current Value必须保证:
Sequence >= MAX(id)十八、问题 12:UPSERT 直接不能用了
MySQL:
INSERT INTO balance(user_id, amount)
VALUES(10001, 100)
ON DUPLICATE KEY UPDATE
amount = VALUES(amount);PostgreSQL:
INSERT INTO balance(user_id, amount)
VALUES(10001, 100)
ON CONFLICT(user_id)
DO UPDATE SET
amount = EXCLUDED.amount;迁移项目中可以直接全局搜索:
ON DUPLICATE KEY UPDATE全部需要处理。
十九、问题 13:LIMIT 写法不同
MySQL:
LIMIT 100, 20意思:
OFFSET = 100
LIMIT = 20PostgreSQL:
LIMIT 20 OFFSET 100所以:
MySQL
LIMIT offset,size
↓
PostgreSQL
LIMIT size OFFSET offset二十、问题 14:大 OFFSET 两边都会慢
例如:
SELECT *
FROM trade
ORDER BY id DESC
LIMIT 100 OFFSET 5000000;数据库实际上:
读取前面大量数据
│
▼
丢掉 5000000 条
│
▼
返回 100 条更好的方式:
SELECT *
FROM trade
WHERE id < 90000000
ORDER BY id DESC
LIMIT 100;也就是:
OFFSET Pagination
↓
Keyset Pagination对于:
订单
成交
资金流水
账单
历史记录尤其重要。
二十一、问题 15:MySQL 的 tinyint(1) 到 PG 怎么处理?
MySQL:
is_deleted TINYINT(1)常见:
0 = false
1 = truePostgreSQL:
is_deleted BOOLEAN直接:
TRUE
FALSE但不要把所有:
tinyint都改成:
boolean例如:
status = 0 Pending
status = 1 Success
status = 2 Failed这种应该:
SMALLINT而不是 BOOLEAN。
二十二、问题 16:JSON 应该迁成 JSON 还是 JSONB?
MySQL:
JSONPostgreSQL:
JSON
JSONB如果 JSON 只是:
保存
读取JSON 可以。
但如果需要:
WHERE 查询
包含判断
索引
字段过滤通常应该重点考虑:
JSONB例如:
SELECT *
FROM orders
WHERE extra @> '{"symbol":"BTCUSDT"}';再结合:
GIN Index所以:
MySQL JSON不要机械:
↓
PG JSON应该:
MySQL JSON
│
├─ 只是保存 → JSON
│
└─ 需要查询 → JSONB二十三、问题 17:时间字段最容易出现“数据没错,但业务错了”
MySQL 常见:
DATETIME
TIMESTAMPPostgreSQL:
timestamp
timestamptz迁移时必须确认:
Application
│
├─ UTC ?
├─ Asia/Shanghai ?
└─ Asia/Tokyo ?
Database
│
└─ TimeZone ?
Server
│
└─ TimeZone ?否则可能出现:
数据库:
2026-08-13 08:00
应用理解:
UTC
实际业务:
UTC+8结果差:
8 Hours交易系统建议:
数据库内部
UTC展示:
UTC
│
├─ Asia/Shanghai
└─ Asia/Tokyo二十四、问题 18:Go 代码不能只换 Driver
MySQL:
db.Query(
"SELECT * FROM users WHERE id = ?",
id,
)PostgreSQL:
db.Query(
"SELECT * FROM users WHERE id = $1",
id,
)也就是:
MySQL
?
PostgreSQL
$1
$2
$3如果项目有几千条手写 SQL:
Driver 替换只是整个迁移的一小部分。
二十五、数据库错误码也要重新改
MySQL:
1062代表:
Duplicate Entry业务可能:
if mysqlErr.Number == 1062 {
// duplicate
}PostgreSQL:
23505代表:
unique_violation常见 PG SQLSTATE:
| 场景 | PostgreSQL |
|---|---|
| 唯一键冲突 | 23505 |
| 外键冲突 | 23503 |
| 非空约束 | 23502 |
| 死锁 | 40P01 |
| 序列化失败 | 40001 |
最好从:
业务代码
│
↓
直接依赖 MySQL Error Code改成:
DB Error Adapter
│
├─ Duplicate
├─ Deadlock
├─ Timeout
├─ ConnectionError
└─ SerializationFailure这样以后数据库迁移会轻松很多。
二十六、MySQL → PostgreSQL 最常见错误迁移方式
最危险的是这种:
第一步:
Schema 转换
第二步:
数据导入
第三步:
SQL改到能运行
第四步:
上线看起来完成了。
实际上应该:
Schema
│
▼
Data Type
│
▼
Data Migration
│
▼
SQL Compatibility
│
▼
Result Verification
│
▼
Index Verification
│
▼
EXPLAIN ANALYZE
│
▼
Transaction Test
│
▼
Lock Test
│
▼
Connection Test
│
▼
Failover Test
│
▼
Pressure Test
│
▼
Production二十七、迁移过程中重点排查表
| 现象 | 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:
数据库慢
│
▼
SHOW PROCESSLIST
│
▼
SHOW ENGINE INNODB STATUS
│
▼
EXPLAIN迁移 PostgreSQL 后:
数据库慢
│
▼
pg_stat_activity
│
├─ Active SQL ?
├─ Long Transaction ?
├─ idle in transaction ?
└─ Lock Wait ?
│
▼
pg_locks
│
▼
EXPLAIN ANALYZE
│
▼
pg_stat_user_tables
│
├─ dead tuples ?
└─ autovacuum ?
│
▼
pg_stat_user_indexes二十九、最后浓缩成一句话
MySQL 迁移 PostgreSQL:
不是
MySQL SQL
↓
改一下语法
↓
PostgreSQL
而是
MySQL System
│
├─ SQL
├─ Data Type
├─ Index
├─ Transaction
├─ Connection Pool
├─ Retry
├─ Timeout
└─ Operation Model
│
▼
PostgreSQL System真正最容易踩坑的可以归纳成:
① 类型更严格
② 事务出错后必须 Rollback
③ 不要照搬 MySQL 连接数
④ Timeout 要区分 SQL / Lock / Connection
⑤ 不要为了兼容到处 CAST
⑥ 索引必须重新 EXPLAIN ANALYZE
⑦ UPDATE / DELETE 要理解 Dead Tuple
⑧ PostgreSQL 必须理解 VACUUM
⑨ 必须监控 Long Transaction
⑩ 必须监控 idle in transaction
⑪ 数据迁移后检查 Sequence
⑫ Deadlock / Serialization Failure 要 Retry
⑬ SQL 能执行 ≠ 性能没问题
⑭ Schema 迁完 ≠ 数据库迁移完成如果原系统属于:
交易所
支付
订单
账户
资金
持仓
清算
账务那么还需要把下面几项提高到最高优先级:
NUMERIC 精度
+
事务一致性
+
行锁竞争
+
Deadlock Retry
+
长事务
+
Connection Pool
+
Autovacuum
+
Table Bloat这些才是 MySQL → PostgreSQL 迁移完成后,最容易在线上真正出问题的地方。
0 评论