为什么从 MySQL 迁移到 PostgreSQL?——功能对比+迁移前的 5 个评估
引言
"我们要不要把数据库从 MySQL 换成 PostgreSQL?"——这个问题在我们团队被反复问了三次:第一次是听说同行都在用 PG,第二次是要做向量检索,第三次是被一个复杂报表 SQL 折磨了一周之后。三次的答案不一样:第一次没迁(纯跟风没有痛点),第二次新业务直接选了 PG(pgvector),第三次核心库做了完整评估后只迁了分析库。
"要不要迁"从来不是技术优劣题,而是成本收益题。PG 确实有大量 MySQL 没有或更强的能力(JSONB、物化视图、PostGIS、扩展生态),但 MySQL 也有它的优势:运维人才多、主从复制简单、大厂云服务成熟、简单读写下性能稳定。盲目迁移的团队往往在第一个月就被"ON UPDATE 居然不自动更新时间戳""字符串不能隐式等于数字"这些差异绊倒。这篇文章回答两个问题:PG 到底强在哪(七个能力点逐项对比),以及动手前必须完成的五个评估,文末附一份可以直接贴在 wiki 上的 MySQL vs PG 语法差异速查表。
一、先给结论:什么情况值得迁
| 情况 | 建议 |
|---|---|
| 标准 CRUD、单库 < 千万级、团队只会 MySQL、没有特殊需求 | 别迁,收益为负,MySQL 完全够用 |
| 需要复杂查询/分析报表、CTE+窗口函数重度使用、对 SQL 表达力有要求 | 新业务库直接 PG;存量评估迁分析库/只读副本 |
| JSON 半结构化数据是核心存储且要频繁查询/索引 | PG(JSONB + GIN 是代差优势) |
| AI/RAG 向量检索、地理空间计算 | PG(pgvector / PostGIS 生态) |
| 需要物化视图、声明式分区、自定义类型/函数的复杂数据场景 | PG |
| 已经重度依赖 MySQL 生态(Canal 监听 binlog、RDS 运维体系、DBA 技能栈) | 谨慎,新业务试点 PG,核心库不动 |
一句话:MySQL 是"优秀的通用 OLTP 数据库",PG 是"可扩展的对象关系型数据平台"。前者把 80% 的常见场景做到简单可靠,后者把剩下 20% 的复杂场景做到极致。迁移的驱动力应该是具体的功能痛点,而不是"PG 更高级"。
二、功能对比:PG 强在哪
2.1 JSONB vs MySQL JSON:半结构化数据的代差
MySQL 5.7 开始支持 JSON 类型,解决了"JSON 能存"的问题;PG 有两种类型:json(存文本)和 jsonb(存解析后的二进制),差距在查询和索引:
-- PostgreSQL:jsonb + GIN 索引,JSON 条件查询直接走索引
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
detail JSONB NOT NULL
);
-- 在整个 JSONB 上建 GIN 索引(通用倒排)
CREATE INDEX idx_orders_detail_gin ON orders USING gin (detail);
-- 包含查询:detail 里存在 address.city = 上海 的订单,走索引
SELECT * FROM orders
WHERE detail @> '{"address": {"city": "上海"}}';
-- 是否包含某个 key / 任意元素
SELECT * FROM orders WHERE detail ? 'couponCode';
-- 按 JSON 字段表达式建索引(只索引某个路径)
CREATE INDEX idx_orders_userid ON orders
(((detail ->> 'userId')::bigint));
| 能力 | MySQL JSON | PostgreSQL JSONB | ||
|---|---|---|---|---|
| 存储 | 二进制 JSON | 二进制(去空格、键去重、可直接比较) | ||
| 查询语法 | JSON_EXTRACT / -> / ->> | -> / ->> / @> / ? / jsonb_path | ||
| 索引 | 多值索引/虚拟生成列索引(要预先规划路径) | GIN 通用索引,任意键查询都能用 | ||
| 包含/存在操作 | 函数嵌套,较繁琐 | @> ? ?| 操作符简洁 | ||
| JSONPath | 支持 | 支持(SQL/JSON 标准 jsonb_path_query) | ||
| 更新 | 部分更新(8.0 优化) | 支持 jsonb_set/ | 合并 | |
实际体验:JSON 结构灵活、查询键不固定的场景(订单快照、营销规则、画像标签),JSONB + GIN 几乎不需要提前设计索引;MySQL 下要为每个查询路径建虚拟列索引,漏一个就全表扫。
2.2 全文检索:内置 tsvector,中文需配分词
-- PG 内置全文检索:文档转向量 + 查询词 + GIN 索引
ALTER TABLE articles ADD COLUMN tsv tsvector;
CREATE INDEX idx_articles_tsv ON articles USING gin (tsv);
-- 英文/默认分词直接可用
UPDATE articles SET tsv = to_tsvector('english', title || ' ' || body);
SELECT title FROM articles
WHERE tsv @@ plainto_tsquery('english', 'database tuning')
ORDER BY ts_rank(tsv, query) DESC;
| 能力 | MySQL FULLTEXT | PG 全文检索 |
|---|---|---|
| 索引 | FULLTEXT 索引 | GIN(还支持 GiST、RUM) |
| 中文 | ngram/MeCab parser 插件 | 需 zhparser/jieba/pg_jieba 扩展 |
| 相关度排序 | 基础 | ts_rank/phrase 检索/ proximity 更灵活 |
| 多语言 | 较弱 | 每种语言一套配置,可自定义词典/停用词/同义词 |
| 与条件组合 | 可以但优化器选择有限 | 和普通 WHERE 自由组合,索引并行使用 |
注意:中文场景两边都需要额外分词组件,PG 的 zhparser(基于 scws)生态成熟;如果中文全文检索是唯一需求,差距没有英文场景大。但 PG 可以把全文检索向量和其他过滤条件(时间、租户)组合进同一条高效查询。
2.3 窗口函数与 CTE:复杂分析 SQL 的表达力
说明:MySQL 8.0 已支持窗口函数和 CTE,这两项不再是"PG 独有",但 PG 的成熟度和高级用法仍然领先:
-- 窗口函数:每个部门薪资排名(两者写法基本一致)
SELECT name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rk
FROM employee;
-- PG 进阶能力:窗口子句(ROWS/RANGE/GROUPS)、frame_exclusion
-- 移动平均、累计占比、同比环比在 PG 里都是标准写法
SELECT dt, gmv,
round(gmv * 100.0 / SUM(gmv) OVER (), 2) AS pct_of_total,
avg(gmv) OVER (ORDER BY dt ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily_sales;
-- CTE 里直接写 DML 并把结果传给后续语句(MySQL 不支持)
WITH moved AS (
DELETE FROM cart WHERE expire_at < now()
RETURNING user_id, sku_id
)
INSERT INTO cart_archive SELECT * FROM moved;
-- 递归 CTE:树形/图查询(组织树、评论盖楼)
WITH RECURSIVE tree AS (
SELECT id, parent_id, name, 1 AS lvl FROM menu WHERE parent_id IS NULL
UNION ALL
SELECT m.id, m.parent_id, m.name, t.lvl + 1
FROM menu m JOIN tree t ON m.parent_id = t.id
)
SELECT * FROM tree;
-- MATERIALIZED 提示:控制 CTE 是否物化(优化器 fence),应对复杂查询计划劣化
WITH hot AS MATERIALIZED (SELECT ... )
| 能力 | MySQL 8 | PostgreSQL |
|---|---|---|
| 窗口函数 | ✅ 基础 | ✅ 完整(GROUPS、frame exclusion、更多窗口) |
| 普通/递归 CTE | ✅ | ✅,且 CTE 内支持 DML+RETURNING |
| CTE 物化控制 | ❌ | ✅ MATERIALIZED/NOT MATERIALIZED |
| LATERAL 派生表关联 | ❌(8.0 后部分支持) | ✅ LATERAL JOIN(每行执行子查询) |
做报表、对账、层级数据查询时,PG 能用一条 SQL 表达 MySQL 里要存储过程+临时表才能完成的逻辑。
2.4 物化视图:MySQL 没有的东西
MySQL 没有原生物化视图(要用定时任务+普通表模拟)。PG 原生支持:
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT date(created_at) AS d, count(*) AS cnt, sum(amount) AS gmv
FROM "order"
GROUP BY date(created_at);
CREATE UNIQUE INDEX idx_mv_daily ON mv_daily_sales(d);
-- 全量刷新(锁表)/ 并发刷新(不阻塞读,需唯一索引)
REFRESH MATERIALIZED VIEW mv_daily_sales;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;
| 对比 | MySQL 模拟方案 | PG 物化视图 |
|---|---|---|
| 实现 | 定时任务 CREATE TABLE AS / 重算 | 数据库原生对象 |
| 查询 | 查模拟表 | 查视图,优化器可见统计 |
| 增量 | 自己写合并逻辑 | 全量/并发刷新;TimescaleDB 连续聚合可自动增量 |
| 适用 | — | 报表、看板、T+1 聚合 |
注意 PG 物化视图本身不自动刷新(这是设计哲学:刷新时机由你控制),需要配定时任务(pg_cron 扩展)或触发器方案。
2.5 分区表:声明式分区已成熟
PG 10 开始声明式分区,现已支持 RANGE/LIST/HASH 和多级子分区,且分区裁剪、默认分区、分区主键约束都完善:
CREATE TABLE log_event (
id BIGINT GENERATED ALWAYS AS IDENTITY,
ts TIMESTAMPTZ NOT NULL,
msg TEXT
) PARTITION BY RANGE (ts);
CREATE TABLE log_event_2026_09 PARTITION OF log_event
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE log_event_default PARTITION OF log_event DEFAULT;
| 维度 | MySQL 分区 | PG 分区 |
|---|---|---|
| 类型 | RANGE/LIST/HASH/KEY | RANGE/LIST/HASH + 多级组合 |
| 分区裁剪 | 支持 | 支持,且执行时裁剪(参数化查询也能裁) |
| 主键限制 | 分区键必须进唯一索引 | 同样要求,但支持默认分区兜底 |
| 运维 | 加分区用 REORGANIZE | ATTACH/DETACH PARTITION,元数据操作秒级 |
| 生态 | — | TimescaleDB 在其上做全自动时序分区 |
2.6 PostGIS:地理信息没有对手
CREATE EXTENSION postgis;
CREATE TABLE store (
id BIGINT PRIMARY KEY,
name TEXT,
location GEOMETRY(Point, 4326) -- SRID 4326 = 经纬度
);
CREATE INDEX idx_store_geo ON store USING gist (location);
-- 查用户 3 公里内的门店,按距离排序,走 GiST 空间索引
SELECT name,
ST_DistanceSphere(location, ST_MakePoint(121.47, 31.23)) AS dist_m
FROM store
WHERE ST_DWithin(location::geography, ST_MakePoint(121.47, 31.23)::geography, 3000)
ORDER BY location <-> ST_MakePoint(121.47, 31.23)
LIMIT 10;
MySQL 有基础 GIS 类型和空间索引,但 PostGIS 是行业事实标准:数百个空间函数(缓冲区、相交、投影转换、路网拓扑、栅格),外卖配送范围、附近的人、电子围栏、地图类业务基本不用比较。
2.7 其他被低估的差异
| 能力 | PG | MySQL |
|---|---|---|
| 扩展机制 | CREATE EXTENSION(pgvector/PostGIS/TimescaleDB/cron…) | 插件生态弱,多靠内核版本迭代 |
| 自定义类型 | 可自定义复合类型/枚举/域 | 枚举有,复杂类型弱 |
| 序列 | SEQUENCE 独立对象,可多表共用/事务关联 | AUTO_INCREMENT 绑列 |
| 返回数据 | INSERT/UPDATE/DELETE ... RETURNING | 8.0 后部分 ROW 别名技巧,无标准 RETURNING |
| 事务 DDL | DDL 在事务内可回滚 | DDL 隐式提交,不可回滚 |
| 约束 | EXCLUDE 排他约束、CHECK(MySQL 8 才强制 CHECK) | CHECK 早期只解析不执行 |
| 并发 | MVCC 实现不同,PG 需关注 vacuum/膨胀 | undo log 模型,回滚段自动 |
三、MySQL 也不是吃素的:迁移会失去什么
客观列一下 MySQL 的优势,这些都是迁移成本:
- 运维生态与人:DBA 市场保有量大,主从复制、半同步、MGR、binlog 备份恢复的资料和工具最多;PG 的 vacuum、复制槽、WAL 膨胀是新学习曲线。
- CDC 生态成熟:Canal、MaxWell 监听 binlog 的生态遍地都是;PG 逻辑复制(wal2json/Debezium)能用,但运维复杂度略高,复制槽积压会导致 WAL 磁盘爆掉。
- 简单高可用门槛低:主从切换工具链成熟;PG 的 Patroni + etcd(或 Consul)方案更强但组件更多。
- 类型宽容:MySQL 隐式转换方便(
where phone = 138...数字字符串不报错),PG 严格类型,老代码里大量"碰巧能跑"的 SQL 会直接报错——这是迁移返工的最大来源。 - 写入模型心智简单:没有 vacuum,不用担心表膨胀、事务 ID 回卷(PG 已大幅改善 autovacuum,但仍需监控)。
四、迁移前的 5 个评估
4.1 评估 1:团队的 SQL 习惯与维护能力
| 检查项 | 说明 |
|---|---|
| 开发是否写标准 SQL | 重度依赖 MySQL 方言(反引号、IF()、DATE_FORMAT、GROUP_CONCAT)的代码越多,改写量越大 |
| 有没有人懂 PG 运维 | vacuum 策略、索引膨胀、WAL、复制槽、pg_hba.conf、连接管理(PG 连接更重,通常要配 PgBouncer) |
| 排障能力 | EXPLAIN 输出完全不同、慢日志换成 pg_stat_statements、锁查询换 pg_locks |
| 建议 | 先在非核心库(报表/新业务)练手 3~6 个月,团队能独立处理膨胀/慢查询后再碰核心库 |
4.2 评估 2:对 MySQL 特有特性的依赖程度(返工重灾区)
把全库 SQL/代码搜一遍,统计以下特性的使用量:
| MySQL 特性 | PG 对应方案 | 改写成本 |
|---|---|---|
| AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY(推荐)或 SERIAL | 低(DDL) |
| ON UPDATE CURRENT_TIMESTAMP | 无对应列属性,必须写触发器 | 中(每个审计字段一个 trigger 函数) |
| INSERT ... ON DUPLICATE KEY UPDATE | INSERT ... ON CONFLICT (...) DO UPDATE SET(更强) | 中(语义要逐条核对) |
| INSERT IGNORE | ON CONFLICT DO NOTHING | 低 |
| REPLACE INTO | ON CONFLICT DO UPDATE(注意 REPLACE 是删+插,自增 ID 会变) | 中,语义差异大 |
| UPDATE ... LIMIT / ORDER BY | 子查询包一层:WHERE id IN (SELECT id ... LIMIT n) | 中 |
| 反引号 ` | 双引号 "(或干脆不转义,PG 无引号标识符折叠为小写) | 低但量大,机械替换 |
| TINYINT(1) 当布尔 | boolean 真类型(true/false) | 中(MyBatis 映射、0/1 比较) |
| IFNULL / IF() | COALESCE / CASE WHEN | 低 |
| GROUP_CONCAT | STRING_AGG(col, ',') | 低 |
| DATE_FORMAT | to_char / to_date | 中(格式串完全不同:%Y→YYYY) |
| DATEDIFF / DATE_ADD | date 直接相减 / + interval '7 days' | 低 |
| LIMIT m, n | LIMIT n OFFSET m(参数顺序相反!) | 中,极易写反 |
| 隐式类型转换('1'=1) | 严格类型,必须显式 ::int/cast | 高,MyBatis 里大量参数类型问题 |
| utf8mb4 | 库级 UTF8(服务端 UTF-8) | 低 |
| ENGINE=InnoDB DEFAULT CHARSET | 删掉,PG 无此概念 | 低 |
| last_insert_id() | RETURNING id | 中(MyBatis useGeneratedKeys 大多自动适配) |
经验值:MyBatis 项目里手写 SQL 越多,迁移工作量越大;JPA/MyBatis-Plus 标准 CRUD 占比越高,越轻松。我们迁移分析库时,40% 的工时花在 ON CONFLICT 语义核对和日期函数改写。
时间戳触发器模板(ON CURRENT_TIMESTAMP 的替代品):
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_order_updated
BEFORE UPDATE ON "order"
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
4.3 评估 3:第三方工具与中间件兼容性
| 组件 | 迁移注意 |
|---|---|
| JDBC 驱动 | mysql-connector-j → org.postgresql:postgresql,连接串/方言配置全换 |
| MyBatis / MyBatis-Plus | CRUD 自动适配;手写 XML SQL、分页方言、${} 拼接要逐一审;MP 的 DbType 改 POSTGRE_SQL |
| JPA/Hibernate | 改方言即可,大部分自动适配(注意 GenerationType.IDENTITY 序列) |
| 连接池 | HikariCP 无感;但 PG 连接开销大(每连接一个进程),生产建议前置 PgBouncer 事务级连接池 |
| ShardingSphere / 分库分表 | 确认版本对 PG 方言支持度 |
| Canal / Debezium CDC | Canal 不支持 PG;用 Debezium PG Connector(逻辑复制 + 复制槽) |
| 备份/监控 | mysqldump→pg_dump;监控模板、Grafana Dashboard、告警规则全部重做 |
| 客户端/数据工具 | DBeaver 无感;自研数据订正脚本、BI 工具连接串全部要改 |
| ORM 里的原生 SQL | 全库搜索 nativeQuery、@Select 注解、JdbcTemplate 字符串 |
4.4 评估 4:停机窗口与迁移方案
| 数据量 | 推荐方案 | 停机时间 |
|---|---|---|
| < 10GB | pgloader 一次性全量迁移,割接窗口停写 | 分钟~十几分钟 |
| 10GB~500GB | pgloader 全量 + Debezium 增量追平 + 短停机割接 | 分钟级 |
| 500GB+ / 零停机要求 | 全量 + CDC 持续增量 + 双写校验 + 灰度切流 | 秒级~零停机 |
pgloader 是 MySQL→PG 的标准工具(不是自己写 JDBC 脚本):自动处理大部分类型映射、批量 COPY 加载、支持迁移索引/外键,还能把不兼容的类型用 cast 规则自定义:
# migrate.load 示例
LOAD DATABASE
FROM mysql://user:pwd@mysql-host:3306/shop
INTO postgresql://user:pwd@pg-host:5432/shop
WITH include drop, create tables, create indexes, reset sequences,
workers = 8, concurrency = 1,
batch rows = 5000, prefetch rows = 10000
CAST TYPE datetime TO timestamptz USING zero-dates-to-null,
TYPE tinyint TO boolean USING tinyint-to-boolean;
ALTER SCHEMA 'shop' RENAME TO 'public';
割接流程(中大型库):
T0 全量:pgloader 迁存量
T0~T1 增量:Debezium 读 MySQL binlog 持续同步到 PG(或自研双写)
T1 校验:行数、checksum、抽样表对比、关键报表双跑对账
T2 停写 MySQL(只读公告/网关切只读)
T3 等待 CDC 延迟归零
T4 应用切 PG 连接,放开写入
T5 观察期(见评估 5)
4.5 评估 5:回滚方案
没有回滚方案的割接都是赌博。回滚的核心问题是:切到 PG 后产生的新数据怎么回 MySQL?
| 手段 | 说明 |
|---|---|
| 反向 CDC | PG 开启逻辑解码,Debezium 把 PG 变更同步回 MySQL(提前搭好并演练) |
| 保留双写 | 割接后一段时间应用层双写(PG 为主、MySQL 异步追),任何问题切回 MySQL 不丢数据;成本高但最稳 |
| MySQL 只读保留 | 至少保留 7~30 天可快速启动的 MySQL 实例(不回收资源),而不是迁完立刻下线 |
| 明确回滚触发条件 | 预先约定:错误率 > X、数据不一致 > Y 笔、核心功能不可用 Z 分钟——值班人有权直接回滚,不用层层审批 |
| 回滚演练 | 割接前在预发完整演练一次"正向迁移+反向回滚",测的是脚本和时间,不是信心 |
建议的切流节奏:灰度 1% → 10% → 50% → 100%,每一档观察业务指标和慢查询/错误日志;回滚时按灰度逆序撤回。
五、迁移后最常踩的 5 个坑
- 标识符大小写:PG 对不加引号的标识符折叠为小写,建表写
CREATE TABLE Order后实际叫 order,查询SELECT * FROM Order报错——规范就是全部小写+下划线,永不加引号。 - 严格类型:
WHERE phone = 13800000000(bigint 列与 numeric 字面量侥幸,varchar 列直接报错)、MyBatis 传 String 给 int 参数全报错。迁移期在测试环境开启全量回归。 - GROUP BY 更严格:SELECT 里的非聚合列必须出现在 GROUP BY 中(MySQL 8 默认 ONLY_FULL_GROUP_BY 也是这规则,但历史代码有大量违规)。
- vacuum/膨胀没人管:迁移后只做备份不监控膨胀,三个月后表膨胀 3 倍性能暴跌——上线即配 autovacuum 参数 + 膨胀监控(pgstattuple)。
- 连接数打满:MySQL 几百连接轻松扛,PG 每连接一个进程,连接数过高上下文切换拖垮整机——应用侧 HikariCP 小池 + 全局 PgBouncer。
六、MySQL vs PG 语法差异速查表(可直接贴 wiki)
| 场景 | MySQL | PostgreSQL |
|---|---|---|
| 自增主键 | id BIGINT AUTO_INCREMENT PRIMARY KEY | id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY |
| 建表后缀 | ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | 无 |
| 字符串转义/引号 | `col` | "col"(建议不加引号,小写下划线) |
| 布尔 | TINYINT(1),0/1 | boolean,true/false |
| 取自增 ID | last_insert_id() / useGeneratedKeys | RETURNING id / useGeneratedKeys |
| Upsert | ON DUPLICATE KEY UPDATE c=VALUES(c) | ON CONFLICT(uk) DO UPDATE SET c=EXCLUDED.c |
| 忽略冲突 | INSERT IGNORE | ON CONFLICT DO NOTHING |
| 删后重插 | REPLACE INTO | ON CONFLICT DO UPDATE(注意语义) |
| 限制更新行数 | UPDATE t SET ... LIMIT 100 | WHERE id IN (SELECT id FROM t LIMIT 100) |
| 分页 | LIMIT 20, 10(第20起10条) | LIMIT 10 OFFSET 20(顺序相反) |
| 空值处理 | IFNULL(a, b) | COALESCE(a, b) |
| 条件表达式 | IF(cond, x, y) | CASE WHEN cond THEN x ELSE y END |
| 字符串聚合 | GROUP_CONCAT(x SEPARATOR ',') | STRING_AGG(x, ',') |
| 日期格式化 | DATE_FORMAT(t, '%Y-%m-%d') | to_char(t, 'YYYY-MM-DD') |
| 当前日期/时间 | CURDATE() / NOW() | CURRENT_DATE / NOW() |
| 日期运算 | DATE_ADD(t, INTERVAL 7 DAY) | t + INTERVAL '7 days' |
| 日期差 | DATEDIFF(a, b)(天) | (a::date - b::date) |
| 类型转换 | CAST(x AS SIGNED) / 隐式转换 | x::bigint(严格,无隐式跨类转换) |
| 布尔判断 | WHERE deleted = 0 | WHERE deleted = false |
| 看库表 | SHOW DATABASES / SHOW TABLES | \l / \dt(或查 information_schema) |
| 看表结构 | DESC t / SHOW CREATE TABLE t | \d t |
| 看执行计划 | EXPLAIN / EXPLAIN ANALYZE(8.0) | EXPLAIN (ANALYZE, BUFFERS) |
| 慢查询 | slow_query_log | pg_stat_statements 扩展 |
| 当前库函数库 | DATABASE() | current_database() |
| 字符长度 | CHAR_LENGTH/LENGTH | length(char_length) |
| 建索引(在线) | ALGORITHM=INPLACE, LOCK=NONE | CREATE INDEX CONCURRENTLY(不能在事务里) |
| 模糊/正则 | LIKE / REGEXP | LIKE / ~(~* 忽略大小写) |
| 注释 | -- 或 # | --(要求后跟空格),/* */ |
| 事务 DDL | 隐式提交不可回滚 | 可在事务中回滚 |
| UPSERT 时的旧值 | VALUES(c) | EXCLUDED.c |
七、常见问题
7.1 MySQL 8 已经这么强了,PG 还有不可替代性吗?
对纯 OLTP CRUD 没有——选哪个都行,团队熟悉哪个用哪个。PG 不可替代的是"扩展平台"能力:pgvector(向量)、PostGIS(地理)、TimescaleDB(时序)、pg_trgm(模糊/相似度)、自定义类型和函数、丰富的全文检索与物化视图。如果你未来 1~2 年确定要碰 AI 检索、地理围栏、复杂分析这三类中的任意一个,PG 的价值就不只是"另一个数据库"。
7.2 能不能两个数据库共存而不是整体迁移?
完全可以,而且这是大多数团队的实际路径:核心交易库继续 MySQL,新建的向量库/画像库/分析库/地理服务用 PG;跨库分析通过 ETL/CDC 汇聚到 PG 数仓。数据库是工具不是信仰,按业务边界选库比"统一技术栈"更重要。代价是团队要维护两套技能和运维体系,小团队慎选。
7.3 pgloader 能保证数据 100% 迁对吗?
不能,它解决 80% 的机械工作,剩下 20% 必须校验:① 类型边界(零日期 0000-00-00 PG 不支持要用 zero-dates-to-null、tinyint 语义、无符号整数溢出 PG 没有 unsigned);② 保留字冲突(order、user 等表名需要处理);③ 字符集和 emoji;④ 索引/外键/默认值表达式差异;⑤ 时区(datetime→timestamptz 的时区解释)。校验手段:行数对账、主键分段 checksum、关键业务报表双跑、应用层影子流量双读对比。
7.4 迁完后性能不如 MySQL 是怎么回事?
90% 是三个原因:① 没跑统计信息:PG 查询计划依赖 ANALYZE 统计,大批量导入后必须 ANALYZE(pgloader 一般会做,手工 COPY 别忘了),否则计划器以为是空表乱选计划;② 索引漏迁或写法不匹配(函数包裹列、隐式转换使索引失效);③ 连接数没控住(没上 PgBouncer,几百连接把内存和 CPU 吃在进程切换上)。用 pg_stat_statements 抓 TOP SQL 逐个分析,不要凭感觉下结论。
7.5 MyBatis 项目迁移工作量怎么量化评估?
三步盘点:① 全局搜索 XML 反引号、ON DUPLICATE、LIMIT m,n、DATE_FORMAT、IFNULL、GROUP_CONCAT、INSERT IGNORE、tinyint——统计命中文件数;② 统计 Mapper 中数据库方言函数的出现频次和涉及的核心链路;③ 按"每个 Mapper 文件 0.5~1 人天 + 集成测试 1 倍人天"粗估。如果手写 SQL 集中在少数大模块(报表/营销),可以只迁这几个库;如果均匀散布在每个交易链路,工作量要乘上完整回归测试的成本。
7.6 PG 的 vacuum 真的很可怕吗?
不可怕但需要理解。PG 的 MVCC 是"更新即插新行+旧行标死",死元组靠 autovacuum 回收。现代 PG(13+)默认 autovacuum 已经相当智能,正常业务开箱即用。需要主动管理的是:高频更新的表调 autovacuum_vacuum_scale_factor(默认 20% 太迟钝,热点表设 1%~5%)、监控膨胀率(pgstattuple/nagios 脚本)、长事务会阻止 vacuum 回收(监控 pg_stat_activity 里的老事务和复制槽积压)。把这三件事加进巡检清单即可。
八、总结
决策速查卡
值得迁:JSONB 半结构化重度查询 / 物化视图+复杂分析 / 向量 / 地理 / 扩展生态
别瞎迁:标准 CRUD + 团队只会 MySQL + 依赖 binlog 生态 + 没有具体痛点
五评估:团队SQL习惯 → MySQL特性依赖(ON UPDATE/ON DUPLICATE/隐式转换)
三方工具(MyBatis手写SQL/CDC/PgBouncer) → 停机窗口(pgloader+CDC)
回滚方案(反向CDC/双写/灰度逆序)
三大坑:标识符小写折叠 / 严格类型报错 / vacuum 膨胀 + 连接数
一句话
"要不要迁 PostgreSQL"的正确答案从来不是 PG 比 MySQL 好,而是你的痛点是否落在 PG 的能力半径里:JSONB 配 GIN 索引让任意键的半结构化查询直接走索引、原生物化视图和支持 DML 的 CTE 让复杂报表一条 SQL 搞定、PostGIS 在地理场景没有对手、pgvector 让 AI 向量和业务数据同库——这四个是"迁了立刻有收益"的硬理由;而窗口函数和 CTE MySQL 8 已经补齐,不算理由。但收益的另一半是成本:ON UPDATE CURRENT_TIMESTAMP 要自己写触发器、ON DUPLICATE KEY 要逐条核对改 ON CONFLICT、LIMIT 参数顺序相反、PG 严格类型让所有依赖隐式转换的 SQL 当场报错、反引号和日期函数全量机械替换,MyBatis 手写 SQL 越多返工越大;运维侧还要吃下 vacuum/膨胀、WAL 复制槽、PgBouncer 连接池三套新心智。所以动迁之前老老实实干完五个评估——团队 SQL 习惯和 PG 运维能力、MySQL 方言特性依赖清单、三方中间件兼容性、pgloader 全量加 Debezium 增量的停机方案、以及带反向 CDC/双写和明确触发条件的回滚方案——然后最稳的路径永远是:新业务用 PG 试点、分析库先迁、核心交易库最后动甚至不动。数据库是工具不是信仰,按业务边界选库,让功能痛点而不是技术潮流拍板。
给团队的建议
| 项 | 建议 |
|---|---|
| 决策 | 列出 3 个具体痛点再讨论迁移,"大家都在用"不算理由 |
| 试点 | 新业务/分析库先行,团队攒 3~6 个月 PG 运维经验 |
| 盘点 | MyBatis XML 全量搜索方言特征,按文件数估工时 |
| 迁移 | pgloader + 类型 cast,迁完立刻 ANALYZE |
| 割接 | 全量+CDC+对账+灰度切流,保留 MySQL 30 天 |
| 回滚 | 反向同步提前演练,触发条件和决策权前置 |
| 上线 | autovacuum 参数、膨胀监控、PgBouncer 同步到位 |
互动话题:你们公司有从 MySQL 迁 PG 的经历吗?最痛的差异是哪一个?是整体迁还是两库共存?评论区聊聊。
参考资料
- PostgreSQL 官方文档(与 MySQL 差异、SQL 语法)
- MySQL 官方文档(迁移工具与兼容性章节)
- pgloader 官方文档(MySQL → PostgreSQL)
- Debezium PostgreSQL Connector(逻辑复制 CDC)
- PostgreSQL Wiki:Don't Do This(从其他库迁入的常见错误)
- pgvector / PostGIS / TimescaleDB 官方文档
标题:为什么从 MySQL 迁移到 PostgreSQL?——功能对比+迁移前的 5 个评估
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/09/22/1789825533465.html
公众号:服务端技术精选
- 引言
- 一、先给结论:什么情况值得迁
- 二、功能对比:PG 强在哪
- 2.1 JSONB vs MySQL JSON:半结构化数据的代差
- 2.2 全文检索:内置 tsvector,中文需配分词
- 2.3 窗口函数与 CTE:复杂分析 SQL 的表达力
- 2.4 物化视图:MySQL 没有的东西
- 2.5 分区表:声明式分区已成熟
- 2.6 PostGIS:地理信息没有对手
- 2.7 其他被低估的差异
- 三、MySQL 也不是吃素的:迁移会失去什么
- 四、迁移前的 5 个评估
- 4.1 评估 1:团队的 SQL 习惯与维护能力
- 4.2 评估 2:对 MySQL 特有特性的依赖程度(返工重灾区)
- 4.3 评估 3:第三方工具与中间件兼容性
- 4.4 评估 4:停机窗口与迁移方案
- 4.5 评估 5:回滚方案
- 五、迁移后最常踩的 5 个坑
- 六、MySQL vs PG 语法差异速查表(可直接贴 wiki)
- 七、常见问题
- 7.1 MySQL 8 已经这么强了,PG 还有不可替代性吗?
- 7.2 能不能两个数据库共存而不是整体迁移?
- 7.3 pgloader 能保证数据 100% 迁对吗?
- 7.4 迁完后性能不如 MySQL 是怎么回事?
- 7.5 MyBatis 项目迁移工作量怎么量化评估?
- 7.6 PG 的 vacuum 真的很可怕吗?
- 八、总结
- 决策速查卡
- 一句话
- 给团队的建议
- 参考资料
评论