1. 为什么需要从MySQL迁移到PostgreSQL?
在数据库选型的十字路口,许多团队都面临过这个抉择。我经历过三次完整的MySQL到PostgreSQL迁移,第一次是2016年某电商平台的订单系统迁移,最近一次是上个月为金融客户做的合规改造。每次迁移的驱动因素都不尽相同,但核心诉求可以归纳为几个方面。
PostgreSQL在复杂查询性能上的优势尤为突出。当我们的电商平台日订单量突破50万时,MySQL在JOIN操作和多层子查询上的性能瓶颈开始显现。一个典型的报表查询,在MySQL 5.7上需要12秒完成,迁移到PostgreSQL 9.6后降至3秒。这得益于PG更先进的查询优化器,特别是对CTE(Common Table Expressions)的原生支持。
数据类型支持是另一个关键差异点。去年我们为生物医药客户处理基因序列数据时,PostgreSQL的数组类型和JSONB直接解决了存储和查询难题。比如存储实验样本的SNP位点数据,在MySQL中需要设计复杂的关联表,而PG只需一个integer[]数组字段,配合GIN索引就能实现高效查询。
重要提示:如果业务中涉及GIS地理信息处理,PostGIS扩展提供的空间数据支持是MySQL无法比拟的,这在物流和IoT领域尤为重要。
事务隔离级别方面,PostgreSQL真正的可串行化隔离(serializable)能有效防止幻读,这对金融级应用至关重要。我们最近处理的账户系统迁移中,MySQL的REPEATABLE READ在并发转账时出现的余额异常,在PG的serializable隔离下得到了完美解决。
从运维角度看,PG的WAL(Write-Ahead Logging)机制提供了更灵活的备份方案。上周我们刚用pg_basebackup实现了生产环境的分钟级恢复,而MySQL的物理备份在TB级数据库上要停机半小时以上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的全面评估与准备
2.1 兼容性差异分析
在启动迁移前,必须建立完整的差异清单。这是我们团队使用的检查表示例:
| 特性类别 | MySQL实现方式 | PostgreSQL对应方案 | 风险等级 |
|---|---|---|---|
| 自增ID | AUTO_INCREMENT | SERIAL/IDENTITY | 高 |
| 字符串比较 | 默认不区分大小写 | 默认区分大小写 | 中 |
| 分页查询 | LIMIT offset, size | LIMIT size OFFSET offset | 低 |
| 日期函数 | DATE_FORMAT() | TO_CHAR() | 中 |
| 布尔类型 | TINYINT(1) | BOOLEAN | 低 |
最容易被忽视的是隐式类型转换差异。去年我们迁移的CMS系统中,发现MySQL会静默将字符串'12abc'转为数字12,而PG会直接报错。这类问题需要通过SET extra_float_digits = 0;等参数调整来缓解。
2.2 工具链选型
经过多次实践,我总结出这些工具的黄金组合:
-
模式迁移:使用pgLoader(支持SSL连接和并行加载)
bash复制
pgloader mysql://user:pass@source_host:3306/dbname \ postgresql://user:pass@target_host:5432/dbname -
数据校验:用自研的DeltaCheck工具(开源版本见GitHub),核心原理是通过CRC32校验分块数据:
python复制def chunk_verifier(mysql_conn, pg_conn, table, chunk_size=10000): # 实现分页CRC比对逻辑 ... -
应用改造:ORM映射层需要特别注意:
- Django:调整
DATABASES配置和使用django.contrib.postgres - Hibernate:修改方言为
org.hibernate.dialect.PostgreSQLDialect
- Django:调整
2.3 性能基准测试
建立双跑环境是必须的。我们在AWS上搭建的测试方案:
- 使用Tcpcopy将生产流量复制到测试环境
- 在PG侧部署Prometheus+Granafa监控体系
- 关键指标对比:
- TPS波动不超过15%
- 95分位延迟差异在20%以内
- 资源消耗(CPU/内存)变化在预期范围内
3. 分步迁移实战手册
3.1 数据库模式转换
字符集问题首当其冲。MySQL的utf8mb4对应PG的UTF8,但要注意排序规则差异。建议在目标库初始化时执行:
sql复制CREATE DATABASE new_db WITH
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8';
表结构转换中的典型问题解决方案:
- 将MySQL的DATETIME转为TIMESTAMP WITH TIME ZONE
- TEXT类型字段在PG中不需要指定长度
- 将ENGINE=InnoDB等存储引擎声明移除
踩坑记录:曾经有客户在MySQL中使用ENUM类型,PG中没有直接对应项。我们最终采用CHECK约束实现:
sql复制CREATE TABLE products ( size TEXT CHECK (size IN ('S', 'M', 'L', 'XL')) );
3.2 数据迁移策略
对于TB级数据库,我们采用分批次迁移方案:
-
初始全量加载:使用pgLoader的batch并发模式
bash复制pgloader --with "batch size = 100MB" --with "batch concurrency = 8" ... -
增量同步阶段:
- 在MySQL侧设置binlog过期时间为7天
- 使用Debezium捕获变更事件
- 通过Kafka将事件转发到PG消费端
-
最终一致性校验:
sql复制-- 在业务低峰期执行 SELECT count(*) AS cnt, sum(checksum(col1,col2,...)) AS chk FROM big_table;
3.3 应用层改造要点
JDBC连接字符串需要调整:
code复制jdbc:postgresql://host:5432/db?stringtype=unspecified&prepareThreshold=0
SQL方言改写示例:
sql复制-- MySQL
SELECT IFNULL(field, 'default') FROM table;
-- PostgreSQL
SELECT COALESCE(field, 'default') FROM table;
事务隔离级别映射:
java复制// Java应用需要显式设置
connection.setTransactionIsolation(
Connection.TRANSACTION_SERIALIZABLE);
4. 迁移后的优化与监控
4.1 性能调优实战
查询计划优化是重中之重。我们发现这三个参数对性能影响最大:
sql复制ALTER SYSTEM SET random_page_cost = 1.1; -- SSD存储环境
ALTER SYSTEM SET effective_cache_size = '12GB'; -- 根据内存调整
ALTER SYSTEM SET work_mem = '64MB'; -- 复杂查询需要更大值
索引策略调整案例:
- 将MySQL的普通索引转为PG的BRIN索引(对于有序的大表)
- 多列查询使用GIN复合索引
- 全文检索改用PG的TSVECTOR
4.2 高可用方案设计
PG的流复制配置示例:
bash复制# 主库postgresql.conf
wal_level = replica
max_wal_senders = 10
# 从库recovery.conf
standby_mode = on
primary_conninfo = 'host=master port=5432 user=replicator'
我们推荐Patroni+etcd的自动故障转移方案,配置要点:
yaml复制scope: pg-cluster
restapi:
listen: 0.0.0.0:8008
etcd:
hosts: ["node1:2379","node2:2379","node3:2379"]
4.3 监控体系搭建
关键监控指标清单:
- 长事务:
SELECT * FROM pg_stat_activity WHERE state = 'active' - 锁等待:
SELECT * FROM pg_locks WHERE granted = false - WAL积压:
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) FROM pg_stat_replication
推荐使用这个Grafana仪表板配置:
json复制{
"panels": [{
"title": "Query Performance",
"targets": [{
"expr": "rate(pg_stat_user_tables_idx_scan[5m])"
}]
}]
}
5. 典型问题解决方案
5.1 字符集乱码问题
遇到乱码时,按这个流程排查:
- 确认客户端编码:
SHOW client_encoding; - 检查服务端编码:
SELECT datname,pg_encoding_to_char(encoding) FROM pg_database; - 必要时强制转换:
sql复制CONVERT(string_content USING 'UTF8')
5.2 时区处理陷阱
我们踩过的坑:某国际业务系统发现时间戳差了8小时。解决方案:
sql复制-- 确保所有会话使用统一时区
SET TIME ZONE 'UTC';
-- 或者在postgresql.conf中设置
timezone = 'UTC'
5.3 大对象迁移方案
对于BLOB数据,推荐使用PG的BYTEA替代:
sql复制-- 导出MySQL blob
SELECT hex(blob_field) FROM table INTO OUTFILE '/tmp/blobs.csv';
-- PG导入
COPY table (bytea_field) FROM '/tmp/blobs.csv' WITH (FORMAT csv);
6. 经验总结与进阶建议
经过多次迁移,我总结出这些黄金法则:
- 永远先在非生产环境完整演练全流程
- 准备回滚方案(特别是数据一致性校验脚本)
- 应用层逐步迁移,采用双写过渡期
性能优化方面,这些技巧很实用:
- 查询
pg_stat_statements找出TOP SQL - 对分析型查询使用
EXPLAIN (ANALYZE, BUFFERS) - 定期执行
VACUUM ANALYZE
最后提醒:PG的扩展生态非常丰富,这些扩展在特定场景下能发挥奇效:
- TimescaleDB:时序数据处理
- PostGIS:地理空间计算
- pg_cron:定时任务管理
- Citus:分布式方案
