1. 数据库迁移的必要性与挑战
十年前我刚入行时,MySQL几乎是互联网项目的默认选择。但最近五年,越来越多的团队开始将目光投向PostgreSQL。上周刚帮一个日活百万的电商平台完成了数据库迁移,整个过程踩了不少坑,也积累了不少实战经验。
为什么大家开始转向PostgreSQL?最直接的驱动力是业务复杂度的提升。当你的系统开始需要处理JSON文档、地理空间数据,或者要保证金融级的事务一致性时,PostgreSQL的特性优势就显现出来了。我见过太多团队在MySQL上不断打补丁,最后发现重构成本已经超过了迁移成本。
但迁移绝不是简单的"导出导入"就能搞定。数据类型差异、SQL语法区别、事务隔离级别的实现方式,这些都会成为迁移路上的绊脚石。去年有个客户在迁移后才发现,他们重度依赖的GROUP BY语句在PostgreSQL中的行为完全不同,导致报表系统完全崩溃。
2. 迁移前的准备工作
2.1 环境评估与兼容性检查
在动手之前,我通常会花2-3天做全面的兼容性评估。首先用pgloader工具的--dry-run模式生成兼容性报告:
bash复制pgloader --dry-run mysql://user:pass@source_host/dbname postgresql://user:pass@target_host/dbname
重点关注以下几类问题:
- 自增ID的处理(MySQL的AUTO_INCREMENT vs PostgreSQL的SERIAL/IDENTITY)
- 字符集和排序规则(特别是utf8mb4的转换)
- 日期时间类型的精度差异
- 索引和约束的命名冲突
重要提示:MySQL的datetime默认值为'0000-00-00'在PostgreSQL中是非法的,必须提前处理
2.2 制定迁移策略
根据数据库规模,我通常推荐三种策略:
| 策略类型 | 适用场景 | 停机时间 | 复杂度 |
|---|---|---|---|
| 一次性迁移 | <50GB | 4-8小时 | 低 |
| 双写过渡 | 50-500GB | 分钟级 | 中 |
| CDC同步 | >500GB | 秒级 | 高 |
对于大多数中型系统,我建议采用双写过渡方案。具体步骤:
- 先全量迁移基础数据
- 应用层开启双写
- 用Debezium同步增量数据
- 验证一致性后切换读操作
3. 核心迁移操作详解
3.1 模式转换与数据类型映射
MySQL的建表语句需要经过转换才能被PostgreSQL接受。这是我总结的常见类型映射表:
| MySQL类型 | PostgreSQL类型 | 注意事项 |
|---|---|---|
| INT | INTEGER | 无差异 |
| VARCHAR | VARCHAR/TEXT | PostgreSQL没有长度限制 |
| DATETIME | TIMESTAMP | 时区处理不同 |
| TINYINT(1) | BOOLEAN | 需要显式转换 |
| ENUM | CREATE TYPE | 需预先创建枚举类型 |
一个典型的转换示例:
sql复制-- MySQL原表
CREATE TABLE users (
id INT AUTO_INCREMENT,
name VARCHAR(50),
status ENUM('active','inactive'),
PRIMARY KEY (id)
);
-- PostgreSQL转换后
CREATE TYPE user_status AS ENUM ('active','inactive');
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT,
status user_status
);
3.2 数据迁移工具选型
经过多次实战测试,我总结的工具对比:
| 工具 | 速度 | 可靠性 | 复杂类型支持 | 推荐场景 |
|---|---|---|---|---|
| pgloader | 快 | 高 | 一般 | 中小型数据库 |
| AWS DMS | 中 | 极高 | 好 | 云环境迁移 |
| 自定义ETL | 慢 | 依赖实现 | 灵活 | 特殊需求 |
对于大多数场景,pgloader是最佳选择。这是我最常用的命令模板:
bash复制pgloader \
--with "prefetch rows = 500" \
--with "workers = 8" \
--with "concurrency = 2" \
mysql://user:pass@mysql_host:3306/dbname \
postgresql://user:pass@pg_host:5432/dbname
关键参数说明:
- prefetch rows:减少网络往返次数
- workers:并行加载的表数量
- concurrency:单个表的并行加载线程数
4. 迁移后的验证与优化
4.1 数据一致性检查
迁移完成后,我必做的三项验证:
- 记录数比对:
sql复制-- MySQL端
SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema = 'dbname';
-- PostgreSQL端
SELECT table_name, reltuples::bigint
FROM pg_class C
LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname = 'public' AND relkind = 'r';
- 抽样数据校验:
python复制# 使用pandas进行抽样比对
import pandas as pd
from sqlalchemy import create_engine
mysql_engine = create_engine('mysql+mysqlconnector://user:pass@mysql_host/dbname')
pg_engine = create_engine('postgresql://user:pass@pg_host/dbname')
def compare_table(table_name, sample_size=1000):
mysql_df = pd.read_sql(f"SELECT * FROM {table_name} ORDER BY RAND() LIMIT {sample_size}", mysql_engine)
pg_df = pd.read_sql(f"SELECT * FROM {table_name} ORDER BY RANDOM() LIMIT {sample_size}", pg_engine)
return mysql_df.compare(pg_df)
- 应用层全量回归测试
4.2 PostgreSQL专属优化
迁移完成后,这些PostgreSQL特有的优化能让性能提升30%以上:
- 合理设置autovacuum参数:
sql复制ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.05;
ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.02;
- 利用部分索引:
sql复制-- 只为活跃用户创建索引
CREATE INDEX idx_users_active ON users(email) WHERE status = 'active';
- JSONB字段的GIN索引:
sql复制CREATE INDEX idx_product_attributes ON products USING GIN (attributes);
5. 常见问题解决方案
5.1 应用层适配问题
这些问题在代码改造时一定会遇到:
- LIMIT子句差异:
sql复制-- MySQL
SELECT * FROM table LIMIT 10, 20;
-- PostgreSQL
SELECT * FROM table LIMIT 20 OFFSET 10;
- 日期函数处理:
sql复制-- MySQL
SELECT DATE_ADD(NOW(), INTERVAL 1 DAY);
-- PostgreSQL
SELECT NOW() + INTERVAL '1 day';
- 连接字符串语法:
java复制// JDBC连接字符串变化
String mysqlUrl = "jdbc:mysql://host:3306/db";
String pgUrl = "jdbc:postgresql://host:5432/db";
5.2 性能下降排查
如果迁移后查询变慢,按这个顺序检查:
- 检查执行计划:
sql复制EXPLAIN ANALYZE SELECT * FROM large_table WHERE create_time > '2023-01-01';
- 统计信息是否准确:
sql复制ANALYZE VERBOSE table_name;
- 检查索引使用情况:
sql复制SELECT * FROM pg_stat_all_indexes WHERE schemaname = 'public';
6. 实战经验分享
经过十几次迁移项目,这些经验是用真金白银换来的:
- 批量插入优化:PostgreSQL的COPY命令比INSERT快10倍以上
sql复制COPY large_table FROM '/path/to/data.csv' WITH (FORMAT csv);
-
事务大小控制:每个事务处理5万-10万条记录最佳,太大容易导致WAL膨胀
-
扩展插件利用:这些插件能解决90%的兼容性问题
sql复制CREATE EXTENSION mysql_fdw; -- 外部表包装器
CREATE EXTENSION uuid-ossp; -- UUID生成
CREATE EXTENSION pg_stat_statements; -- 性能监控
- 监控指标设置:这些指标必须监控
- 锁等待时间
- 长事务数量
- WAL文件增长速率
最后提醒一点:一定要在非高峰期进行最终切换,并且准备好回滚方案。我曾经遇到过一个案例,因为Nginx的DNS缓存导致部分流量仍然指向旧数据库,造成了数据不一致。现在我的标准做法是在切换后立即将MySQL用户权限改为只读,并在DNS中设置较短的TTL值。