从MySQL迁移到PostgreSQL

在数据库选型中,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_INCREMENTON 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 迁移后查询性能下降

问题:迁移后应用查询速度变慢,尤其是复杂查询。

解决方案

    1. 分析表统计信息:PostgreSQL需手动更新表统计信息,优化查询计划:
sql 复制代码
ANALYZE user_order;
    1. 优化索引:使用EXPLAIN ANALYZE分析慢查询,添加缺失索引(如联合索引、GIN索引)。
    1. 调整PostgreSQL参数:根据服务器配置优化shared_buffers(建议设为物理内存的25%)、work_mem等参数。

四、迁移后优化:提升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 应用适配优化

    1. 驱动替换:将MySQL驱动替换为PostgreSQL驱动(以Java为例):
xml 复制代码
# Maven依赖替换(MySQL → PostgreSQL)
    1. 连接字符串修改:
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的高级特性优化业务:

    1. 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"}';
    1. 分区表:对大表(如订单表)创建分区,提升查询和维护效率:
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归档),构建更稳定、高效的数据库架构。

相关推荐
是萝卜干呀2 小时前
Redis
数据库·redis·缓存
摇滚侠2 小时前
数据库类型有哪些,除了关系型数据库,还有哪些类型,列一个表格,并列出该类型的代表数据库管理系统
数据库
切糕师学AI2 小时前
DBeaver + PostgreSQL 中的 Global Backup 和 Backup 的区别?
数据库·postgresql·dbeaver
繁星星繁2 小时前
【Mysql】数据库基础
android·数据库·mysql
ttthe_MOon2 小时前
MySQL 基础:索引的定义与作用
数据库·mysql
心动啊1212 小时前
简单学下chromaDB
开发语言·数据库·python
妮妮喔妮3 小时前
redis热点key拆分和读多副本
数据库·redis·缓存
雪球不会消失了3 小时前
MySQL(开发篇)
数据库·mysql·oracle