在数据库选型中,MySQL因轻量易用、生态成熟长期占据主流地位,但随着业务发展,不少团队会面临高并发、复杂查询、数据一致性要求提升等问题。PostgreSQL作为功能全面的开源数据库,在事务稳定性、复杂查询优化、JSON支持、扩展性等方面具备显著优势,成为很多企业的迁移首选。本文将结合实战经验,梳理从MySQL迁移到PostgreSQL的完整路线,涵盖迁移前准备、核心迁移步骤、常见问题解决及迁移后优化,并附上详细示例代码,帮助读者快速落地迁移工作。
一、迁移前准备:打好基础,避免踩坑
迁移前的准备工作直接决定迁移效率和成功率,核心要解决"迁什么""用什么迁""迁前怎么防风险"三个问题,具体可分为以下3个核心环节:
1.1 环境与业务调研
首先需明确迁移的核心范围和约束条件,避免盲目操作:
-
版本兼容性确认:PostgreSQL对低版本兼容性较好,但建议目标版本选择12+(LTS长期支持版),稳定性更有保障;MySQL源版本需注意5.6+与8.0的差异(8.0新增的JSON、窗口函数等特性迁移时需特殊处理)。
-
业务依赖梳理:统计所有连接MySQL的应用(Web服务、定时任务、ETL脚本等),记录连接方式(JDBC/ODBC/ORM框架)、访问频率、核心业务表(如订单表、用户表)及峰值时段,避免迁移期间影响核心业务。
-
数据范围界定:确认是否迁移全量数据(含历史归档数据),是否需要分阶段迁移(先非核心表,再核心表),是否保留源库备份用于回滚。
1.2 迁移工具选型
根据数据量大小、Schema复杂度选择合适的工具,常用工具对比及适用场景如下:
| 工具名称 | 核心优势 | 适用场景 | 注意事项 |
|---|---|---|---|
| pgLoader | 开源免费、配置简单、支持增量迁移、自动处理部分数据类型映射 | 中小数据量(100G以内)、Schema不复杂的场景 | 需提前安装依赖,对MySQL存储过程支持有限 |
| AWS DMS | 支持异构数据库迁移、可配置CDC增量同步、稳定性高 | 大数据量(TB级)、需零停机迁移的企业级场景 | 收费服务,配置门槛较高 |
| 手动迁移(CSV导出导入) | 可控性强、适配复杂Schema、无工具依赖 | 小表迁移、特殊表(含复杂索引/约束)迁移 | 效率低,需手动处理数据类型和语法差异 |
| 本文以最常用的pgLoader 和手动迁移结合为例,覆盖大部分实战场景。 |
1.3 源库备份与环境搭建
迁移前必须做好数据备份,避免数据丢失:
- MySQL源库备份:使用mysqldump全量备份,示例代码如下(含存储过程、触发器):
bash
# 全量备份MySQL数据库(含存储过程、触发器、事件)
mysqldump -u root -p --all-databases --routines --triggers --events > mysql_full_backup_20251210.sql
# 仅备份指定数据库(如test_db)
mysqldump -u root -p test_db --routines --triggers --events > test_db_backup_20251210.sql
- 目标库环境搭建:安装PostgreSQL(以CentOS 7为例),并创建对应数据库和用户:
bash
# 安装PostgreSQL 15
yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
yum install -y postgresql15 postgresql15-server
# 初始化数据库
/usr/pgsql-15/bin/postgresql-15-setup initdb
# 启动服务并设置开机自启
systemctl start postgresql-15
systemctl enable postgresql-15
# 切换到postgres用户,创建迁移用数据库和用户
su - postgres
psql
# 创建用户(与MySQL用户名一致,便于应用适配)
CREATE USER test_user WITH PASSWORD 'test_pass123';
# 创建数据库(编码UTF-8,与MySQL一致)
CREATE DATABASE test_db WITH OWNER test_user ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';
# 授权
GRANT ALL PRIVILEGES ON DATABASE test_db TO test_user;
二、核心迁移步骤:从Schema到数据的全量迁移
迁移核心流程为:Schema迁移 → 数据迁移 → 索引/约束迁移 → 存储过程/触发器迁移,需重点处理MySQL与PostgreSQL的语法和特性差异。
2.1 Schema迁移:最易踩坑的核心环节
Schema(表结构)迁移的关键是解决数据类型映射、语法差异问题。建议先导出MySQL的表结构SQL,手动适配PostgreSQL语法后执行,避免工具自动迁移的遗漏。
2.1.1 数据类型映射表(核心对照)
| MySQL数据类型 | PostgreSQL对应类型 | 注意事项 |
|---|---|---|
| INT/INTEGER | INTEGER | 完全兼容,无差异 |
| BIGINT | BIGINT | 完全兼容,无差异 |
| VARCHAR(n) | VARCHAR(n) | PostgreSQL中n表示字符数(MySQL中部分版本表示字节数),需确认编码一致 |
| DATETIME | TIMESTAMP WITHOUT TIME ZONE | PostgreSQL的TIMESTAMP默认无时区,如需时区用TIMESTAMP WITH TIME ZONE |
| DATE | DATE | 完全兼容 |
| DECIMAL(p,s) | NUMERIC(p,s) | PostgreSQL中DECIMAL是NUMERIC的别名,可直接使用 |
| JSON/JSONB | JSONB | PostgreSQL的JSONB支持索引,查询效率更高,建议优先使用 |
| TINYINT(1) | BOOLEAN | MySQL中TINYINT(1)常用作布尔值,PostgreSQL建议用原生BOOLEAN类型 |
2.1.2 表结构迁移示例(对比改写)
以MySQL的user_order表为例,展示迁移前后的SQL改写:
sql
# MySQL原表结构SQL
CREATE TABLE `user_order` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`order_no` varchar(32) NOT NULL COMMENT '订单号',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '订单状态:0-待支付,1-已支付',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_no` (`order_no`),
KEY `idx_user_id_create_time` (`user_id`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订单表';
PostgreSQL适配后SQL(重点修改点已标注):
sql
# PostgreSQL适配后表结构SQL
CREATE TABLE user_order (
id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) COMMENT '订单ID', -- 1. AUTO_INCREMENT → GENERATED ALWAYS AS IDENTITY
user_id BIGINT NOT NULL COMMENT '用户ID',
order_no VARCHAR(32) NOT NULL COMMENT '订单号',
amount NUMERIC(10,2) NOT NULL COMMENT '订单金额', -- 2. decimal → NUMERIC
status BOOLEAN NOT NULL DEFAULT FALSE COMMENT '订单状态:false-待支付,true-已支付', -- 3. TINYINT(1) → BOOLEAN
create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', -- 4. ON UPDATE 需用触发器实现(PostgreSQL无此语法)
PRIMARY KEY (id),
UNIQUE CONSTRAINT idx_order_no UNIQUE (order_no), -- 5. UNIQUE KEY → UNIQUE CONSTRAINT
INDEX idx_user_id_create_time (user_id, create_time) -- 6. KEY → INDEX(语法一致,可直接保留)
) COMMENT='用户订单表'; -- 7. 去掉ENGINE/CHARSET(PostgreSQL默认UTF8,引擎无需指定)
关键说明:PostgreSQL不支持MySQL的AUTO_INCREMENT和ON UPDATE CURRENT_TIMESTAMP语法,需分别用GENERATED ALWAYS AS IDENTITY(自增)和触发器(更新时间自动更新)替代。
2.2 数据迁移:全量同步与一致性校验
Schema创建完成后,进行数据迁移。小数据量可手动导出CSV导入,大数据量建议用pgLoader工具高效迁移。
2.2.1 工具迁移(pgLoader)示例
pgLoader支持通过配置文件指定迁移规则,自动处理部分数据类型映射:
bash
# 1. 安装pgLoader(CentOS 7为例)
yum install -y pgloader
# 2. 创建迁移配置文件(mysql_to_pg.load)
vi mysql_to_pg.load
# 配置文件内容
LOAD DATABASE
FROM mysql://test_user:test_pass123@192.168.1.100:3306/test_db -- 源MySQL地址
INTO postgresql://test_user:test_pass123@127.0.0.1:5432/test_db -- 目标PostgreSQL地址
WITH include drop, create tables, create indexes, reset sequences, -- 迁移选项:删除目标表、创建表、创建索引、重置序列
workers = 4, concurrency = 4, -- 并发数(根据服务器配置调整)
cast type datetime to timestamp without time zone, -- 手动指定类型映射(覆盖默认规则)
cast type tinyint(1) to boolean; -- TINYINT(1) → BOOLEAN
# 3. 执行迁移
pgloader mysql_to_pg.load
2.2.2 手动迁移(CSV导出导入)示例
适合小表或工具迁移失败的表,可控性更强:
bash
# 1. MySQL导出CSV数据(以user_order表为例)
mysql -u test_user -p -e "SELECT * FROM test_db.user_order INTO OUTFILE '/tmp/user_order.csv' FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\n';"
# 2. 授权PostgreSQL访问CSV文件(PostgreSQL默认限制文件访问)
chmod 777 /tmp/user_order.csv
su - postgres
psql -d test_db
# 3. PostgreSQL导入CSV数据
COPY user_order (id, user_id, order_no, amount, status, create_time, update_time)
FROM '/tmp/user_order.csv'
DELIMITER ','
ENCLOSED BY '"'
CSV HEADER; -- HEADER表示CSV第一行为列名(若导出时未包含,去掉此参数)
2.2.3 数据一致性校验
迁移后必须校验数据完整性,避免遗漏或错误:
sql
# 1. 行数对比(MySQL与PostgreSQL分别执行)
-- MySQL
SELECT COUNT(*) FROM test_db.user_order;
-- PostgreSQL
SELECT COUNT(*) FROM user_order;
# 2. 关键字段求和/最大值对比(以amount为例)
-- MySQL
SELECT SUM(amount), MAX(amount) FROM test_db.user_order;
-- PostgreSQL
SELECT SUM(amount), MAX(amount) FROM user_order;
# 3. 抽样查询对比(随机取10条数据)
-- PostgreSQL(MySQL语法类似)
SELECT * FROM user_order ORDER BY RANDOM() LIMIT 10;
2.3 索引/约束与存储过程迁移
这部分是迁移的"收尾工作",需重点处理语法差异:
2.3.1 索引迁移补充
大部分索引语法MySQL与PostgreSQL兼容,但PostgreSQL支持更丰富的索引类型(如GIN、GiST),可针对性优化:
sql
# MySQL中的普通索引/唯一索引可直接迁移,PostgreSQL新增JSONB索引示例(优化拓展)
-- 若表中有JSONB字段(如user_info),创建GIN索引提升查询效率
CREATE INDEX idx_user_info_jsonb ON user_order USING GIN (user_info);
2.3.2 触发器迁移示例(解决ON UPDATE问题)
MySQL的ON UPDATE CURRENT_TIMESTAMP需用PostgreSQL触发器实现:
sql
# 1. 创建触发器函数(更新update_time字段)
CREATE OR REPLACE FUNCTION update_modify_time()
RETURNS TRIGGER AS $$
BEGIN
NEW.update_time = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
# 2. 给user_order表创建触发器
CREATE TRIGGER trg_user_order_update_time
BEFORE UPDATE ON user_order
FOR EACH ROW
EXECUTE FUNCTION update_modify_time();
2.3.3 存储过程迁移示例(语法改写)
MySQL与PostgreSQL的存储过程语法差异较大,需手动改写,以"查询用户订单总数"为例:
sql
# MySQL原存储过程
DELIMITER //
CREATE PROCEDURE get_user_order_count(IN user_id BIGINT, OUT order_count INT)
BEGIN
SELECT COUNT(*) INTO order_count FROM user_order WHERE user_id = user_id;
END //
DELIMITER ;
# PostgreSQL改写后存储过程
CREATE OR REPLACE PROCEDURE get_user_order_count(IN p_user_id BIGINT, OUT p_order_count INTEGER)
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*) INTO p_order_count FROM user_order WHERE user_id = p_user_id;
END;
$$;
三、常见问题与解决方案:实战避坑指南
迁移过程中难免遇到各种问题,以下是高频问题及解决方案:
3.1 数据类型不兼容导致迁移失败
问题 :MySQL的DATETIME字段包含非法值(如'0000-00-00 00:00:00'),PostgreSQL不支持该格式,迁移报错。
解决方案:迁移前清理MySQL中的非法时间值,或在pgLoader配置中指定替换规则:
bash
# pgLoader配置中添加替换规则(将'0000-00-00 00:00:00'替换为'1970-01-01 00:00:00')
LOAD DATABASE
...
SET MySQL PARAMETERS
sql_mode = 'ALLOW_INVALID_DATES',
datetime_format = '%Y-%m-%d %H:%i:%s'
WITH cast type datetime to timestamp without time zone using (
case when source = '0000-00-00 00:00:00' then '1970-01-01 00:00:00' else source end
);
3.2 大小写敏感问题导致表/列无法访问
问题:MySQL默认不区分表名/列名大小写(Linux环境下需配置),PostgreSQL默认区分,迁移后应用查询报错"relation "user_order" does not exist"。
解决方案:
-
方案1:统一表名/列名为小写(推荐),迁移时修改Schema中的表名/列名。
-
方案2:修改PostgreSQL配置文件(postgresql.conf),关闭大小写敏感:
bash
# 编辑配置文件
vi /var/lib/pgsql/15/data/postgresql.conf
# 修改参数(默认是off,改为on表示不区分大小写)
lower_case_table_names = on
# 重启PostgreSQL
systemctl restart postgresql-15
3.3 迁移后查询性能下降
问题:迁移后应用查询速度变慢,尤其是复杂查询。
解决方案:
-
- 分析表统计信息:PostgreSQL需手动更新表统计信息,优化查询计划:
sql
ANALYZE user_order;
-
- 优化索引:使用
EXPLAIN ANALYZE分析慢查询,添加缺失索引(如联合索引、GIN索引)。
- 优化索引:使用
-
- 调整PostgreSQL参数:根据服务器配置优化
shared_buffers(建议设为物理内存的25%)、work_mem等参数。
- 调整PostgreSQL参数:根据服务器配置优化
四、迁移后优化:提升PostgreSQL性能与稳定性
迁移完成不代表结束,需针对性优化,充分发挥PostgreSQL的优势:
4.1 数据库参数优化(基础配置)
bash
# 编辑postgresql.conf
vi /var/lib/pgsql/15/data/postgresql.conf
# 核心参数优化(适用于8G内存服务器)
shared_buffers = 2GB # 共享内存,建议物理内存的25%
work_mem = 64MB # 每个排序/哈希操作的内存上限
maintenance_work_mem = 512MB # 维护操作(如VACUUM)的内存上限
effective_cache_size = 6GB # 数据库期望的缓存大小(建议物理内存的75%)
max_connections = 200 # 最大连接数(根据应用需求调整)
# 重启生效
systemctl restart postgresql-15
4.2 应用适配优化
-
- 驱动替换:将MySQL驱动替换为PostgreSQL驱动(以Java为例):
xml
# Maven依赖替换(MySQL → PostgreSQL)
-
- 连接字符串修改:
java
# MySQL连接字符串
jdbc:mysql://192.168.1.100:3306/test_db?useSSL=false&serverTimezone=UTC
# PostgreSQL连接字符串
jdbc:postgresql://192.168.1.200:5432/test_db?sslmode=disable&TimeZone=UTC
4.3 高级特性拓展(发挥PostgreSQL优势)
迁移后可利用PostgreSQL的高级特性优化业务:
-
- JSONB字段索引:对JSONB类型字段创建GIN索引,提升查询效率:
sql
-- 表中添加JSONB字段
ALTER TABLE user_order ADD COLUMN ext_info JSONB COMMENT '订单扩展信息';
-- 创建GIN索引
CREATE INDEX idx_ext_info ON user_order USING GIN (ext_info);
-- 高效查询JSONB字段
SELECT * FROM user_order WHERE ext_info @> '{"pay_type": "alipay"}';
-
- 分区表:对大表(如订单表)创建分区,提升查询和维护效率:
sql
# 创建按月份分区的订单表
CREATE TABLE user_order (
-- 同前文表结构
) PARTITION BY RANGE (create_time);
# 创建2025年12月分区
CREATE TABLE user_order_202512 PARTITION OF user_order
FOR VALUES FROM ('2025-12-01') TO ('2026-01-01');
五、总结与展望
从MySQL迁移到PostgreSQL的核心是"先准备再迁移,先Schema再数据,迁移后必校验、必优化"。迁移过程中需重点关注数据类型映射、语法差异和性能适配,工具(如pgLoader)可提升效率,但核心差异点仍需手动处理。
迁移完成后,建议分阶段切换业务流量(先非核心业务,再核心业务),持续监控数据库性能,逐步引入PostgreSQL的高级特性(如JSONB、分区表、全文搜索),充分发挥其在复杂场景下的优势。
后续会进一步探索PostgreSQL的高可用方案(如主从复制、Patroni)、备份策略(如pg_dump、WAL归档),构建更稳定、高效的数据库架构。