MySQL 老兵的 PostgreSQL 上岸指南:7 天上手,一条命令搬家
先说结论
上一篇我们聊了为什么越来越多的项目正从 MySQL 搬进 PostgreSQL,这一篇解决「怎么搬」。对 MySQL 老兵来说,PostgreSQL 不是一门新语言,而是一种「方言相近的口音」:80% 的 SQL 肌肉记忆可以直接带过来,真正要重学的只有三件事——概念模型(database/schema 的户型差异)、方言翻译(自增、布尔、引号、分页)、以及 pgloader 这条「搬家公司流水线」。本文给出一张 7 天学习路线图、一份方言速查表、一次真实的 pgloader 迁移实录(含我踩过的认证大坑)、一套生产环境无缝迁移六步曲,以及 Windows 11 / Ubuntu 26.04 / macOS 26 三套一键沙盒脚本,全部本地 Docker 完成,不依赖任何第三方云服务。
图 1:本文封面(自制)。上一篇讲「为什么搬」,这一篇讲「怎么搬、怎么学」。
一、问题背景:老板说要迁 PG,MySQL 老兵怎么办
上一篇文章发出来后,有读者留言说:「道理我都懂,PG 确实香,但我们团队全是 MySQL 老兵,DBA 会修主从延迟、会调 innodb_buffer_pool,就是没摸过 psql。老板说下个季度核心系统要迁 PostgreSQL,我该怎么办?」
这是当下很多团队的真实处境:技术选型是架构师拍的板,落地执行的却是每一个写 SQL、值夜班的人。选型对了不代表迁移能成,中间隔着一条叫「团队技能栈」的鸿沟。
好消息是:MySQL 和 PostgreSQL 都是关系型数据库,共享同一套 SQL 世界观。你不需要重新学数据库,只需要做一次「方言培训」。坏消息是:如果没人告诉你方言的坑在哪,你会在最意想不到的地方翻车——比如一条在 MySQL 里跑了五年的 GROUP BY,到 PG 里直接报错。
这篇文章就是那份「方言培训教材」。
二、问题表现:MySQL 老兵的五大水土不服
先建立一个直观的对照。我在测试环境搭了一个典型的 MySQL「老店」:AUTO_INCREMENT 主键、TINYINT(1) 当布尔、ENUM 存状态、JSON 存画像、ON UPDATE CURRENT_TIMESTAMP 自动更新——全是 MySQL 的经典口音:

图 2:真实截图。这张 users 表几乎集齐了 MySQL 方言的所有代表作,后面我们就拿它开刀,看 pgloader 怎么逐个翻译。
带着这样的肌肉记忆走进 PostgreSQL,老兵通常会撞上五堵墙:
第一堵墙:客户端不会用。 mysql -u root -p 变成了 psql -U postgres -d dbname;SHOW TABLES 变成了 \dt;SHOW CREATE TABLE 变成了 \d tablename;DESCRIBE 没了。第一天 80% 的时间在查「PG 里 XXX 命令怎么写」。
第二堵墙:database 的概念对不上。 MySQL 里 USE shop 一切换,世界就变了;PG 里 shop 可能是一个 database,也可能只是一个 schema,两者权限模型完全不同(下一节细讲,这是最容易搬错的一点)。
第三堵墙:大小写和引号。 MySQL 里 `UserName` 用反引号,表名在 Linux 下默认区分大小写、Windows 下不区分;PG 里用双引号,而且不加引号的标识符一律被折叠成小写——CREATE TABLE UserName 创建出来的其实是 username。ORM 生成的大小写混合表名,是迁移翻车的重灾区。
第四堵墙:事务里报一次错,整个事务就死了。 MySQL 里事务中一条语句失败,其他语句还能继续;PG 里事务一旦出错,直接报 current transaction is aborted, commands ignored until end of transaction block,必须回滚。很多老代码的「先 INSERT 试试,撞唯一键就 UPDATE」写法,在 PG 里要改成 INSERT ... ON CONFLICT。
第五堵墙:GROUP BY 是严格的。 MySQL(早期版本或非严格模式下)允许 SELECT user_id, nickname, COUNT(*) FROM orders GROUP BY user_id——nickname 不在 GROUP BY 里也能查出来,随机给你一行。PG 严格执行 SQL 标准:不在 GROUP BY 里也不在聚合函数里的列,一律报错。
三、问题分析:先把「户型图」看懂
这堵墙里最值钱的是第二堵,我们展开讲。打个比方:MySQL 的实例是一栋公寓楼,每个 database 是一层楼;PostgreSQL 的集群也是一栋楼,每层楼(database)里还隔着一个个房间(schema)。
图 3:自制图解。记住这句口诀:MySQL 的「库」,搬到 PG 里大多数时候应该建成「schema」,而不是「database」。
这个差异直接决定迁移策略:
- MySQL 里跨库联查
SELECT * FROM shop.orders JOIN blog.users ...稀松平常,因为楼层之间串门自由; - PG 里同一个 database 内跨 schema 查询同样自由(
shop.orders JOIN blog.users),但跨 database 查询要借道 dblink 或 postgres_fdw,等于从一楼到二楼得下楼走楼梯再上来; - 所以迁移时如果无脑地把每个 MySQL database 建成一个 PG database,原来的跨库 JOIN 全部阵亡。正确姿势是:一个业务建一个 PG database,MySQL 的多个库映射成 PG 的多个 schema。
事实上,「MySQL 用户搬到 PG 要先搞懂什么」这个话题,PG 官方维基有一篇著名的文章叫 Things to find out about when moving from MySQL to PostgreSQL:

图 4:真实截图。注意左上角「Last updated 8th April 2001」——这份搬家指南写了二十五年了。两个数据库的恩怨情仇,比很多读者的工龄都长。
四、怎么学:MySQL 老兵的 7 天上手路线
学 PG 最好的类比是从 iPhone 换到 Android:打电话、发微信的肌肉记忆全部保留,要重学的只是设置在哪、通知怎么管、以及「为什么这个按钮换了个位置」。基于这个思路,我给 MySQL 老兵设计了一条 7 天路线:
图 5:自制图解。核心原则是「先沙盒、后生产」——就像换手机前先云备份,演练不是浪费时间,是买保险。
- D1 装环境:Docker 起一套 PG,学会
psql三连:\dt(看表)、\d 表名(看结构)、\l(看库)。今天唯一的目标是「敢在 psql 里走路」。 - D2 概念映射:搞懂 cluster / database / schema / role 四个概念,把第三节的户型图背下来。
- D3 SQL 方言:自增、分页、引号、布尔、时间类型,配合第五节的速查表过一遍。
- D4 事务与锁:重点体验 PG 的事务型 DDL——改表结构可以回滚,这是运维人员的「后悔药」:

图 6:真实截图。BEGIN 之后 ALTER TABLE 加了一列 motto(8 行变 8 列),ROLLBACK 之后列消失(回到 7 列)。整个过程就像购物车里加了商品又删掉,货架本身从没被动过。MySQL 的 DDL 大多自带隐式提交,反悔的机会都没有。
- D5 pgloader 迁移:在沙盒里把一个 MySQL 库完整搬到 PG,命令可重放。
- D6 校验与压测:行数核对、抽样比对、
EXPLAIN ANALYZE看慢查询。 - D7 切换演练:把「停写→追平→切连接串→回滚」完整走一遍,掐表计时。
学习资料方面,一本官方文档就够:PostgreSQL 18 Documentation 的 Tutorial 章节是给「会 SQL 但不会 PG」的人写的,恰好就是你:

图 7:真实截图。页头还能看到 2026 年 8 月刚发布的 18.6——文档和软件是同一天更新的,这就是社区驱动项目的心跳。
五、方言翻译速查表
这一节是全文最适合收藏的部分。MySQL 腔和 PG 腔的对照,就像两地方言:说的都是 SQL,口音不一样:
图 8:自制图解。pgloader 能自动翻译 80% 的口音,剩下 20%(存储过程、触发器、ON UPDATE 默认值)要人工翻。
除了图上这些,还有几个高频考点:
| MySQL 腔 | PostgreSQL 腔 | 备注 |
|---|---|---|
LIMIT 10, 20 |
LIMIT 20 OFFSET 10 |
PG 不支持逗号写法,用标准语法 |
ON DUPLICATE KEY UPDATE |
ON CONFLICT ... DO UPDATE |
PG 语法更显式,功能更强 |
NOW()、CURDATE() |
now()、CURRENT_DATE |
大部分同名可用 |
IFNULL(a, b) |
COALESCE(a, b) |
标准 SQL 写法 |
IF(cond, a, b) |
CASE WHEN cond THEN a ELSE b END |
PG 没有 IF() 函数 |
反引号 `col` |
双引号 "col" |
不加引号一律折叠为小写 |
SHOW PROCESSLIST |
SELECT * FROM pg_stat_activity |
查连接现场 |
六、迁移实战:pgloader 一条命令搬家
工具选型不用纠结,pgloader 是事实标准——它的口号就是「Migrate to PostgreSQL in a single command!」:

图 9:真实截图。6.5k star,还在活跃维护(最近在做 v4 重写)。注意它不只支持 MySQL,SQLite、MS SQL、CSV 都能搬。
pgloader 的用法真的就一条命令:
pgloader mysql://migrator:密码@源库地址/shop \
postgresql://postgres:密码@目标库地址/shop_pg
它做的事远比 mysqldump 导来导去聪明:自动读取 MySQL 的元数据,把 AUTO_INCREMENT 翻译成序列、TINYINT(1) 翻译成真布尔、ENUM 翻译成 PG 枚举类型,然后用 COPY 协议高速灌数据,最后重建索引、重置序列。我在测试环境的真实运行记录:

图 10:真实截图。注意中间那两行 shop.orders 和 shop.users:4 行加 3 行数据,0 错误,总耗时 0.272 秒。像搬家公司给你的验收单——搬了几件、坏了几个、花了多久,一目了然。
搬完之后验收,重点看方言翻译的质量:

图 11:真实截图。三张图的信息量很大:表落在了 shop schema 下;is_vip 里的 1/0 变成了真布尔 t/f;JSON 画像可以用 ->>'city' 直接取值。最下面 \d shop.users 可以看到 id 列已经挂上了 nextval 序列——自增也翻译过来了。
不过验收单里也藏着两个「翻译腔」,必须人工干预:
DATETIME被翻译成了timestamp with time zone(timestamptz)。pgloader 的默认转换规则把 MySQL 的 naive 时间当成 UTC 处理,如果你的应用存的是本地时间,搬完数据会整体偏移 8 小时。解法是在迁移命令里加转换规则:CAST column users.created_at to timestamp drop typemod(对每列显式指定,或用自定义 command file 批量指定)。ON UPDATE CURRENT_TIMESTAMP丢了。PG 的列默认值没有这个语义,需要写一个触发器补回来,或者干脆让应用层在 UPDATE 时显式带上updated_at = now()——后者更简单,也更符合现代 ORM 的习惯。
另外注意 json 翻译成了 PG 的 json 而不是更强的 jsonb。如果业务要按 JSON 内部字段建索引或高频查询,建议迁移后手动 ALTER TABLE ... ALTER COLUMN profile TYPE jsonb USING profile::jsonb 升级。
七、我踩过的坑:MySQL 8.x 的认证插件
这次实战我结结实实踩了一个坑,值得单独一节。pgloader 一连 MySQL 就报:

图 12:真实截图。MYSQL-UNSUPPORTED-AUTHENTICATION——pgloader 内置的 MySQL 客户端库不认识 MySQL 8.x 默认的 caching_sha2_password 认证插件。就像搬家公司的老卡车刷不开新小区的蓝牙门禁。
排查路径:MySQL 8.0 起默认认证插件换成了 caching_sha2_password(8.4 里老的 mysql_native_password 插件甚至被默认禁用),而 pgloader 3.6.x 内置的 qmynd 客户端只认识老式握手。解法两步:
-- 1. MySQL 8.0 启动参数加:--default-authentication-plugin=mysql_native_password
-- (MySQL 8.4 还需加 --mysql-native-password=ON 启用老插件)
-- 2. 单独建一个迁移专用账号,用老式认证:
CREATE USER 'migrator'@'%' IDENTIFIED WITH mysql_native_password BY '只用于迁移的强密码';
GRANT ALL PRIVILEGES ON shop.* TO 'migrator'@'%';
两个额外建议:迁移账号只授权要搬的库,用完即删;生产环境如果动不了默认插件,可以用 pgloader 的 Docker 镜像在沙盒先把数据导成 PG dump,再灌进生产。
八、生产环境怎么搬:无缝迁移六步曲
沙盒里一条命令的事,到了生产环境就是一场战役。核心矛盾是:数据是活的,你搬的时候它还在长。好比搬家公司在卡车开走之后,你又陆续网购了十个包裹——没有增量同步机制,这十个包裹永远送不到新家。
图 13:自制图解。每一步都有验收单,任何一步不合格就回滚。口诀:先盘点、再试搬、全量加增量、对账后切流、旧家别急着退租。
补充几个工程细节:
- 盘点阶段用
information_schema.ROUTINES和TRIGGERS表扫出所有存储过程和触发器——它们是 pgloader 的盲区,需要人工改写为 PL/pgSQL; - 增量同步中小规模可以用「停写窗口 + 二次 pgloader」的朴素方案(表数据按主键范围追平);大库再上 Debezium 这类 CDC 方案;
- 切流阶段真正的停机时间 = 增量追平时间 + 改连接串时间,练熟了可以压进 5 分钟;
- 回滚通道必须提前演练:切流后 MySQL 保持只读但不退役,PG 侧的新增数据要能反向回灌(至少备好导出脚本),确认稳定 2~4 周再退租。
九、一键沙盒:三分钟搭好你的迁移练习场
纸上得来终觉浅。下面三套脚本在你自己的机器上用 Docker 一键搭出「MySQL 老店 + PostgreSQL 新家 + pgloader 搬家公司」的完整沙盒,并自动完成一次迁移和验收。只依赖本机 Docker,不连任何第三方服务。
9.1 人工执行
Windows 11(PowerShell,需 Docker Desktop 已启动),保存为 Start-Mysql2PgLab.ps1:
# Start-Mysql2PgLab.ps1 — MySQL→PostgreSQL 迁移沙盒(Windows 11)
$ErrorActionPreference = "Stop"
$Net = "mysql2pg-lab"; $Pass = "LabOnly!123"
docker info | Out-Null
docker network create $Net 2>$null | Out-Null
docker rm -f lab-mysql, lab-pg 2>$null | Out-Null
docker run -d --name lab-mysql --network $Net -e MYSQL_ROOT_PASSWORD=$Pass `
mysql:8.0 --default-authentication-plugin=mysql_native_password | Out-Null
docker run -d --name lab-pg --network $Net -e POSTGRES_PASSWORD=$Pass postgres:18 | Out-Null
Write-Host "等待 MySQL 就绪..."
do { Start-Sleep 3; $ok = docker exec lab-mysql mysqladmin ping -uroot -p$Pass --silent 2>$null } until ($LASTEXITCODE -eq 0)
$Seed = @"
CREATE DATABASE shop CHARACTER SET utf8mb4; USE shop;
CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nickname VARCHAR(64) NOT NULL, is_vip TINYINT(1) NOT NULL DEFAULT 0,
level ENUM('bronze','silver','gold') NOT NULL DEFAULT 'bronze', profile JSON,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);
INSERT INTO users (nickname,is_vip,level,profile) VALUES
('alice',1,'gold',JSON_OBJECT('city','shanghai')),('bob',0,'silver',NULL);
CREATE USER 'migrator'@'%' IDENTIFIED WITH mysql_native_password BY '$Pass';
GRANT ALL PRIVILEGES ON shop.* TO 'migrator'@'%';
"@
$Seed | docker exec -i lab-mysql mysql -uroot -p$Pass
docker exec lab-pg psql -U postgres -c "CREATE DATABASE shop_pg;" | Out-Null
Write-Host "开始 pgloader 迁移..."
docker run --rm --network $Net dimitri/pgloader:latest pgloader `
"mysql://migrator:$Pass@lab-mysql/shop" "postgresql://postgres:$Pass@lab-pg/shop_pg"
docker exec lab-pg psql -U postgres -d shop_pg -c "\dt shop.*" -c "SELECT * FROM shop.users;"
Write-Host "完成!清理:docker rm -f lab-mysql lab-pg"
Ubuntu 26.04(Bash,需已装 Docker),保存为 start-mysql2pg-lab.sh:
#!/usr/bin/env bash
# start-mysql2pg-lab.sh — MySQL→PostgreSQL 迁移沙盒(Ubuntu 26.04)
set -euo pipefail
NET=mysql2pg-lab; PASS='LabOnly!123'
docker info >/dev/null
docker network create "$NET" 2>/dev/null || true
docker rm -f lab-mysql lab-pg 2>/dev/null || true
docker run -d --name lab-mysql --network "$NET" -e MYSQL_ROOT_PASSWORD="$PASS" \
mysql:8.0 --default-authentication-plugin=mysql_native_password >/dev/null
docker run -d --name lab-pg --network "$NET" -e POSTGRES_PASSWORD="$PASS" postgres:18 >/dev/null
echo "等待 MySQL 就绪..."
until docker exec lab-mysql mysqladmin ping -uroot -p"$PASS" --silent 2>/dev/null; do sleep 3; done
docker exec -i lab-mysql mysql -uroot -p"$PASS" <<'SQL'
CREATE DATABASE shop CHARACTER SET utf8mb4; USE shop;
CREATE TABLE users (id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nickname VARCHAR(64) NOT NULL, is_vip TINYINT(1) NOT NULL DEFAULT 0,
level ENUM('bronze','silver','gold') NOT NULL DEFAULT 'bronze', profile JSON,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);
INSERT INTO users (nickname,is_vip,level,profile) VALUES
('alice',1,'gold',JSON_OBJECT('city','shanghai')),('bob',0,'silver',NULL);
CREATE USER 'migrator'@'%' IDENTIFIED WITH mysql_native_password BY 'LabOnly!123';
GRANT ALL PRIVILEGES ON shop.* TO 'migrator'@'%';
SQL
docker exec lab-pg psql -U postgres -c "CREATE DATABASE shop_pg;" >/dev/null
echo "开始 pgloader 迁移..."
docker run --rm --network "$NET" dimitri/pgloader:latest pgloader \
"mysql://migrator:$PASS@lab-mysql/shop" "postgresql://postgres:$PASS@lab-pg/shop_pg"
docker exec lab-pg psql -U postgres -d shop_pg -c '\dt shop.*' -c 'SELECT * FROM shop.users;'
echo "完成!清理:docker rm -f lab-mysql lab-pg"
macOS 26(zsh,需 Docker Desktop 或 colima 已启动),保存为 start-mysql2pg-lab.zsh,内容与 Ubuntu 版几乎相同,首行换成 #!/bin/zsh,并加一句启动检测:
#!/bin/zsh
# start-mysql2pg-lab.zsh — MySQL→PostgreSQL 迁移沙盒(macOS 26)
if ! docker info >/dev/null 2>&1; then
echo "Docker 未就绪:请启动 Docker Desktop,或 brew 装 colima 后执行 colima start"
exit 1
fi
set -euo pipefail
# ……余下部分与 Ubuntu 脚本完全相同……
三个脚本跑完都会看到和本文图 10、图 11 一样的输出——那说明你的练习场搭好了,接下来把 shop 换成你自己业务的库重演一遍即可。
9.2 Agent 自动配置
如果你有 Kimi Code、Claude Code、Codex 或其他能执行本机命令的 Agent,直接把下面这段指令给它:
请在我的本机用 Docker 搭建一个 MySQL→PostgreSQL 迁移练习沙盒并完成一次迁移演示。要求:1) 用 docker network 启动 mysql:8.0(加 –default-authentication-plugin=mysql_native_password)和 postgres:18 两个容器;2) 在 MySQL 中建一个含 AUTO_INCREMENT、TINYINT(1)、ENUM、JSON 字段的示例库 shop,插入不少于 3 行数据;3) 创建 mysql_native_password 认证的迁移账号,只授权 shop 库;4) 用 dimitri/pgloader 镜像把 shop 迁到 PostgreSQL 的 shop_pg 库;5) 在 psql 里用 \dt 和 SELECT 验收行数与类型转换结果;6) 全程不要使用宿主机的真实业务数据,不要输出任何内网地址、机器名或密钥;7) 完成后告诉我每个容器的名称和清理命令。
这段 prompt 的关键在约束条件:限定只用 Docker 容器、禁用真实数据、要求交付清理命令——Agent 像搬家公司的临时工,活儿能干,但你得先告诉它「别进主卧」。
十、Q&A
Q1:pgloader 会搬存储过程和触发器吗?
不会。它只管表结构、数据、索引和约束。MySQL 的存储过程要人工改写成 PL/pgSQL,这也是迁移项目里工作量评估最容易漏的一项——盘点阶段先把 information_schema.ROUTINES 扫一遍。
Q2:几百 GB 的大库,pgloader 还顶得住吗?
顶得住,但要调参:workers、prefetch rows、batch size 都会影响吞吐;超大表建议按主键范围分批。真正的瓶颈通常不在工具而在停机窗口,大库请走第八节的「全量 + 增量」路线。
Q3:应用代码要改多少?
取决于 SQL 方言的浓度。纯 ORM(MyBatis/JPA/Prisma)项目通常只换驱动和连接串;手写 SQL 多的项目,重点搜 LIMIT x,y、ON DUPLICATE KEY、IFNULL、反引号这四类。建议在 CI 里加一条「PG 方言静态检查」。
Q4:迁完性能会不会掉?
默认参数的 PG 偏保守,生产环境至少要调 shared_buffers、work_mem、max_connections 三件套,并配上 PgBouncer 连接池。另外 PG 的表膨胀(bloat)需要关注 autovacuum 健康,这和 InnoDB 的运维直觉完全不同,DBA 要补课。
Q5:能不能先在 PG 跑起来,MySQL 留着双跑? 可以,这正是第八节的「并行对账」阶段。注意双写方案里要让 MySQL 依然是事实源(source of truth),PG 侧数据视为可丢弃的副本,对账差异以 MySQL 为准修 PG,直到差异率归零再切换。
Q6:学习路上最值得先读的官方文档是哪几章? Tutorial 全章、Chapter 5(Data Definition)、Chapter 13(Concurrency Control,讲透 MVCC)、以及 psql 的完整参考页。配合 7 天路线,一周足够建立地图。
十一、结尾
MySQL 老兵转 PostgreSQL,最难的从来不是技术,是心理——总觉得「我 MySQL 还没吃透呢」。但这两个数据库的关系,更像手动挡和自动挡:你离合换挡的肌肉记忆不会浪费,它们让你更快理解变速箱在干什么。
所以别把这次迁移当成「从头再学一门数据库」,把它当成一次搬家的机会:旧家里那些用顺手的工具(SQL 功底、索引直觉、慢查询排查经验)全部带走,新家还附赠一墙乐高(pgvector、PostGIS、事务型 DDL)。7 天上手,30 天迁完——下一篇,我们聊聊搬进新家之后的「装修」:PG 生产调优的十个开关。
参考资料:pgloader 官方仓库、PostgreSQL 官方文档、PostgreSQL 维基:Things to find out about when moving from MySQL to PostgreSQL、pgloader MySQL→PostgreSQL 实践指南、Docker + pgloader 迁移教程。