中文 English

MySQL 老兵的 PostgreSQL 上岸指南:7 天上手,一条命令搬家

发布时间: 2026-08-22 · 阅读量 --
PostgreSQL MySQL 数据库 database 数据迁移 教程

先说结论

上一篇我们聊了为什么越来越多的项目正从 MySQL 搬进 PostgreSQL,这一篇解决「怎么搬」。对 MySQL 老兵来说,PostgreSQL 不是一门新语言,而是一种「方言相近的口音」:80% 的 SQL 肌肉记忆可以直接带过来,真正要重学的只有三件事——概念模型(database/schema 的户型差异)、方言翻译(自增、布尔、引号、分页)、以及 pgloader 这条「搬家公司流水线」。本文给出一张 7 天学习路线图、一份方言速查表、一次真实的 pgloader 迁移实录(含我踩过的认证大坑)、一套生产环境无缝迁移六步曲,以及 Windows 11 / Ubuntu 26.04 / macOS 26 三套一键沙盒脚本,全部本地 Docker 完成,不依赖任何第三方云服务。

自制封面:MySQL 老兵把钥匙交给 PostgreSQL 新家。

图 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 的经典口音:

真实终端截图:MySQL 8.0 里的典型业务表结构。

图 2:真实截图。这张 users 表几乎集齐了 MySQL 方言的所有代表作,后面我们就拿它开刀,看 pgloader 怎么逐个翻译。

带着这样的肌肉记忆走进 PostgreSQL,老兵通常会撞上五堵墙:

第一堵墙:客户端不会用。 mysql -u root -p 变成了 psql -U postgres -d dbnameSHOW TABLES 变成了 \dtSHOW CREATE TABLE 变成了 \d tablenameDESCRIBE 没了。第一天 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)。

自制图解:MySQL 的楼层模型与 PostgreSQL 的楼层+房间模型。

图 3:自制图解。记住这句口诀:MySQL 的「库」,搬到 PG 里大多数时候应该建成「schema」,而不是「database」。

这个差异直接决定迁移策略:

事实上,「MySQL 用户搬到 PG 要先搞懂什么」这个话题,PG 官方维基有一篇著名的文章叫 Things to find out about when moving from MySQL to PostgreSQL

真实截图:PostgreSQL 官方维基的搬家指南。

图 4:真实截图。注意左上角「Last updated 8th April 2001」——这份搬家指南写了二十五年了。两个数据库的恩怨情仇,比很多读者的工龄都长。

四、怎么学:MySQL 老兵的 7 天上手路线

学 PG 最好的类比是从 iPhone 换到 Android:打电话、发微信的肌肉记忆全部保留,要重学的只是设置在哪、通知怎么管、以及「为什么这个按钮换了个位置」。基于这个思路,我给 MySQL 老兵设计了一条 7 天路线:

自制图解:7 天学习路线图。

图 5:自制图解。核心原则是「先沙盒、后生产」——就像换手机前先云备份,演练不是浪费时间,是买保险。

真实终端截图:PostgreSQL 事务型 DDL 演示。

图 6:真实截图。BEGIN 之后 ALTER TABLE 加了一列 motto(8 行变 8 列),ROLLBACK 之后列消失(回到 7 列)。整个过程就像购物车里加了商品又删掉,货架本身从没被动过。MySQL 的 DDL 大多自带隐式提交,反悔的机会都没有。

学习资料方面,一本官方文档就够:PostgreSQL 18 Documentation 的 Tutorial 章节是给「会 SQL 但不会 PG」的人写的,恰好就是你:

真实截图:PostgreSQL 18 官方文档首页。

图 7:真实截图。页头还能看到 2026 年 8 月刚发布的 18.6——文档和软件是同一天更新的,这就是社区驱动项目的心跳。

五、方言翻译速查表

这一节是全文最适合收藏的部分。MySQL 腔和 PG 腔的对照,就像两地方言:说的都是 SQL,口音不一样:

自制图解:MySQL 与 PostgreSQL 的方言速查。

图 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!」:

真实截图:pgloader 的 GitHub 仓库。

图 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 协议高速灌数据,最后重建索引、重置序列。我在测试环境的真实运行记录:

真实终端截图:pgloader 迁移全过程输出。

图 10:真实截图。注意中间那两行 shop.ordersshop.users:4 行加 3 行数据,0 错误,总耗时 0.272 秒。像搬家公司给你的验收单——搬了几件、坏了几个、花了多久,一目了然。

搬完之后验收,重点看方言翻译的质量:

真实终端截图:psql 里的迁移验收。

图 11:真实截图。三张图的信息量很大:表落在了 shop schema 下;is_vip 里的 1/0 变成了真布尔 t/f;JSON 画像可以用 ->>'city' 直接取值。最下面 \d shop.users 可以看到 id 列已经挂上了 nextval 序列——自增也翻译过来了。

不过验收单里也藏着两个「翻译腔」,必须人工干预:

  1. DATETIME 被翻译成了 timestamp with time zone(timestamptz)。pgloader 的默认转换规则把 MySQL 的 naive 时间当成 UTC 处理,如果你的应用存的是本地时间,搬完数据会整体偏移 8 小时。解法是在迁移命令里加转换规则:CAST column users.created_at to timestamp drop typemod(对每列显式指定,或用自定义 command file 批量指定)。
  2. 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 就报:

真实终端截图:caching_sha2_password 认证报错现场。

图 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:自制图解。每一步都有验收单,任何一步不合格就回滚。口诀:先盘点、再试搬、全量加增量、对账后切流、旧家别急着退租。

补充几个工程细节:

九、一键沙盒:三分钟搭好你的迁移练习场

纸上得来终觉浅。下面三套脚本在你自己的机器上用 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 还顶得住吗? 顶得住,但要调参:workersprefetch rowsbatch size 都会影响吞吐;超大表建议按主键范围分批。真正的瓶颈通常不在工具而在停机窗口,大库请走第八节的「全量 + 增量」路线。

Q3:应用代码要改多少? 取决于 SQL 方言的浓度。纯 ORM(MyBatis/JPA/Prisma)项目通常只换驱动和连接串;手写 SQL 多的项目,重点搜 LIMIT x,yON DUPLICATE KEYIFNULL、反引号这四类。建议在 CI 里加一条「PG 方言静态检查」。

Q4:迁完性能会不会掉? 默认参数的 PG 偏保守,生产环境至少要调 shared_bufferswork_memmax_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 PostgreSQLpgloader MySQL→PostgreSQL 实践指南Docker + pgloader 迁移教程

本文阅读量 --