MySQL 迁移 PostgreSQL 常见问题与避坑指南

By karp 4 Views 65 MIN READ 0 Comments

MySQL 迁移 PostgreSQL,真正麻烦的地方通常不是 CRUD 语法。

而是迁移之后发现:

连接突然满了
SQL 超时了
索引不走了
事务卡死了
程序断线了
自增 ID 冲突了
同样 SQL 跑得比 MySQL 慢
UPDATE 越跑表越大

所以这篇文章不从数据库理论开始,而是直接按线上常见问题横向对比。


一、先看总表

问题MySQLPostgreSQL迁移时最容易踩的坑
连接数可以相对较高单连接成本更高直接照搬 MySQL 连接池
连接超时connect_timeout客户端 connect timeout把连接超时误认为 SQL 慢
SQL执行超时max_execution_timestatement_timeoutPG 默认策略不同
锁等待innodb_lock_wait_timeoutlock_timeoutSQL 本身不慢,是在等锁
空闲连接wait_timeoutidle_session_timeout连接池拿到失效连接
空闲事务相对不突出idle in transaction 非常危险长事务阻塞 VACUUM
断线server has gone awayserver closed connection没设计连接池重连
类型转换比较宽松非常严格varchar = bigint 直接报错
事务错误某些错误后还能继续一条失败,事务进入 abortedcatch 后继续执行
死锁InnoDB 检测PG 检测只调大 timeout
UPSERTON DUPLICATE KEYON CONFLICTSQL 无法直接兼容
自增AUTO_INCREMENTSequence / Identity导数据后 Sequence 没同步
Booleantinyint(1)boolean0/1 字段迁移错误
JSONJSONJSONB 更常用机械迁移成 JSON
分页LIMIT offset,sizeLIMIT size OFFSET offsetSQL 方言不同
索引B-Tree 为主BTree / GIN / GiST / BRIN索引照搬但执行计划变了
UPDATEInnoDB Undo新 Tuple Version高频更新产生 Dead Tuple
垃圾回收PurgeVACUUM不理解 VACUUM
表膨胀有但感知较弱更需要关注UPDATE/DELETE 后磁盘暴涨
执行计划EXPLAINEXPLAIN ANALYZE 很强只看有没有索引,不看实际执行

二、问题 1:连接数突然打满

MySQL

以前可能是:

10 个服务
每个服务 100 个连接

= 1000 Connections

MySQL:

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

对应:

类型MySQLPostgreSQL
建立连接connect timeoutconnect timeout
SQL执行max_execution_timestatement_timeout
锁等待innodb_lock_wait_timeoutlock_timeout
空闲连接wait_timeoutidle_session_timeout
空闲事务无完全对应idle_in_transaction_session_timeout

排查 Timeout 时不要直接:

Timeout
 ↓
加索引

应该:

Timeout
 │
 ├─ 没拿到连接?
 │
 ├─ 建连接失败?
 │
 ├─ 在等锁?
 │
 ├─ SQL 真慢?
 │
 └─ 网络问题?

五、问题 3:断线重连

MySQL 常见

MySQL server has gone away
Lost connection to MySQL server

PostgreSQL 常见

server closed the connection unexpectedly
connection reset by peer
broken 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 = BIGINT

MySQL 很多情况下会自动转换:

'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 lock

PostgreSQL:

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 Version

PostgreSQL:

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 transaction

PG 中:

Long Transaction
      │
      ▼
Old Snapshot
      │
      ▼
旧 Tuple 不能正常回收
      │
      ▼
Vacuum 受影响
      │
      ▼
Dead Tuple 增长
      │
      ▼
Table Bloat

所以 MySQL 迁移 PostgreSQL 后:

连接监控

不能只看:

active connection

还要重点看:

idle in transaction

十七、问题 11:导完数据后,自增 ID 冲突

MySQL:

AUTO_INCREMENT

比如:

当前最大 ID:

100000

下一条:

100001

PostgreSQL 常用:

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  = 20

PostgreSQL:

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 = true

PostgreSQL:

is_deleted BOOLEAN

直接:

TRUE
FALSE

但不要把所有:

tinyint

都改成:

boolean

例如:

status = 0 Pending
status = 1 Success
status = 2 Failed

这种应该:

SMALLINT

而不是 BOOLEAN。


二十二、问题 16:JSON 应该迁成 JSON 还是 JSONB?

MySQL:

JSON

PostgreSQL:

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
TIMESTAMP

PostgreSQL:

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秒慢SQLLock / statement timeout
DB连接满max_connectionsPool / 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_INCREMENTSequence 未同步
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 迁移完成后,最容易在线上真正出问题的地方。

本文由 karp 原创

采用 CC BY-NC-SA 4.0 协议进行许可

转载请注明出处:https://www.ikarp.top/index.php/archives/873.html

标签: mysql

0 评论