1. 为什么需要从MySQL迁移到PostgreSQL?
在数据库选型的十字路口,越来越多的团队开始重新审视PostgreSQL的价值。作为从业15年的全栈工程师,我经历过7次不同规模的数据迁移,其中最近3次都是从MySQL转向PostgreSQL。这个趋势背后有几个关键驱动因素:
PostgreSQL的JSONB类型在处理半结构化数据时,性能比MySQL的JSON类型快2-3倍。去年我们电商平台的商品属性查询,迁移后响应时间从平均87ms降到了32ms。特别是在处理深层嵌套查询时,PostgreSQL的GIN索引优势明显。
事务隔离级别方面,PostgreSQL真正的可串行化隔离解决了MySQL REPEATABLE READ下的幻读问题。金融项目中遇到的一个典型场景:对账系统在并发处理时,MySQL需要额外加锁才能保证数据一致性,而PostgreSQL原生支持就足够。
重要提示:如果业务重度依赖MySQL特有的语法如GROUP_CONCAT,需要评估改写成本。我们曾有个报表系统因此多花了2周适配。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的关键技术评估
2.1 数据类型映射陷阱
迁移过程中最耗时的往往不是表结构转换,而是数据类型差异的处理。以下是几个典型案例:
-
MySQL的DATETIME精度只到秒,而PostgreSQL的TIMESTAMP可以到微秒。我们物流系统的轨迹记录表就因此丢失了毫秒级时间戳,后来通过修改应用层代码补救。
-
MySQL的TINYINT(1)会被ORM框架自动转成布尔值,但PostgreSQL不会。这导致我们用户系统的状态字段全部异常,最终用CASE WHEN在迁移脚本中显式转换。
-
TEXT类型在MySQL中最大支持64KB,而PostgreSQL没有这个限制。这本是优势,但有个历史系统依赖MySQL的截断行为,导致迁移后出现意外的大数据插入。
2.2 索引与查询优化器差异
PostgreSQL的查询优化器更智能,但也更敏感。有个分页查询在MySQL执行只要5ms:
sql复制SELECT * FROM orders WHERE user_id=123 LIMIT 10 OFFSET 20;
在PostgreSQL上却要300ms,因为优化器误判了user_id的基数。通过强制使用索引扫描解决:
sql复制SELECT * FROM orders WHERE user_id=123 ORDER BY id LIMIT 10 OFFSET 20;
复合索引的顺序也影响巨大。MySQL可以乱序使用索引最左前缀,而PostgreSQL更严格。我们有个联合索引(a,b,c),在MySQL中WHERE b=?也能部分利用索引,但PostgreSQL必须包含a列。
3. 实战迁移流程详解
3.1 使用pgloader进行数据迁移
经过多次实践,我总结出pgloader的最佳配置模板:
bash复制pgloader \
mysql://user:pass@source_host:3306/dbname \
postgresql://user:pass@target_host:5432/dbname \
--with "batch size=10000" \
--with "prefetch rows=50000" \
--with "workers=8" \
--with "concurrency=4"
关键参数说明:
- batch size控制每次提交的记录数,太大容易OOM,太小影响性能
- prefetch rows影响内存占用,50000条约消耗2GB内存
- workers数建议是CPU核心数的2倍
血泪教训:曾经在32核机器上开64个worker导致系统崩溃。建议先用1/4资源测试,逐步调优。
3.2 存储过程重写指南
MySQL的存储过程语法与PostgreSQL的PL/pgSQL差异显著:
- 变量声明方式不同:
sql复制-- MySQL
DECLARE var INT DEFAULT 0;
-- PostgreSQL
var INTEGER := 0;
- 循环语法差异:
sql复制-- MySQL
WHILE condition DO
-- logic
END WHILE;
-- PostgreSQL
WHILE condition LOOP
-- logic
END LOOP;
- 异常处理对比:
sql复制-- MySQL
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
-- PostgreSQL
BEGIN
-- logic
EXCEPTION WHEN OTHERS THEN
ROLLBACK;
END;
我们300多个存储过程平均每个需要2-3小时重写,最复杂的风控计算过程花了3天。
4. 迁移后的验证与优化
4.1 数据一致性校验
开发了专用校验工具检查以下维度:
- 记录数比对(允许±0.1%差异)
- 随机采样字段值校验
- 聚合函数结果对比(SUM/AVG等)
- 事务边界测试
发现的主要问题:
- MySQL的GROUP BY不严格,可能返回随机记录,而PostgreSQL要求明确
- 字符集转换导致的中文乱码(需统一用UTF-8)
- 自增ID序列在并发插入时顺序不一致
4.2 性能调优实战
PostgreSQL的配置优化要点:
conf复制# postgresql.conf关键修改
shared_buffers = 8GB # 25% of total RAM
effective_cache_size = 24GB # 75% of total RAM
maintenance_work_mem = 2GB # 大型索引构建时提升
random_page_cost = 1.1 # SSD存储配置
max_worker_processes = 8 # 并行查询用
我们通过pg_stat_statements找出了TOP 10慢查询:
sql复制SELECT query, calls, total_time, rows
FROM pg_stat_statements
ORDER BY total_time DESC LIMIT 10;
针对性地添加了部分覆盖索引,并使用CTE重写了复杂查询。最终查询性能平均提升40%,其中有个数据分析查询从12秒降到0.8秒。
5. 业务适配经验分享
5.1 ORM框架调整
Hibernate配置变化示例:
xml复制<!-- MySQL方言 -->
<property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
<!-- PostgreSQL方言 -->
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQL95Dialect</property>
MyBatis的注意事项:
- 分页语法需要调整:
sql复制-- MySQL
LIMIT #{offset}, #{size}
-- PostgreSQL
LIMIT #{size} OFFSET #{offset}
- 批量插入语法不同:
sql复制-- MySQL
INSERT INTO table VALUES (1,'a'),(2,'b');
-- PostgreSQL
INSERT INTO table VALUES (1,'a');
INSERT INTO table VALUES (2,'b');
-- 或者用COPY命令
5.2 监控体系改造
我们新增了这些PostgreSQL特有监控项:
- 长事务检测:
sql复制SELECT pid, now()-xact_start AS duration, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
- 锁等待分析:
sql复制SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid;
- WAL日志监控:
bash复制# 检查WAL目录空间使用
du -sh /var/lib/postgresql/12/main/pg_wal
6. 回滚方案设计
即使准备充分,我们仍建议准备回滚方案。我们的checklist包括:
-
数据同步双写:
- 使用Debezium捕获PostgreSQL变更
- 实时同步回MySQL备用库
-
功能开关配置:
java复制// 配置中心控制读写路径
if (featureToggle.isPGEnabled()) {
// 走PostgreSQL
} else {
// 走MySQL
}
- 灰度发布策略:
- 先迁移只读从库
- 然后迁移非核心业务
- 最后迁移核心交易库
在实际迁移中,我们遇到JSON处理不兼容问题,通过双写方案平稳回退,用48小时修复问题后再次迁移。这个备用方案避免了业务中断。
