为什么从 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 JSONPostgreSQL 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 FULLTEXTPG 全文检索
索引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 8PostgreSQL
窗口函数✅ 基础✅ 完整(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/KEYRANGE/LIST/HASH + 多级组合
分区裁剪支持支持,且执行时裁剪(参数化查询也能裁)
主键限制分区键必须进唯一索引同样要求,但支持默认分区兜底
运维加分区用 REORGANIZEATTACH/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 其他被低估的差异

能力PGMySQL
扩展机制CREATE EXTENSION(pgvector/PostGIS/TimescaleDB/cron…)插件生态弱,多靠内核版本迭代
自定义类型可自定义复合类型/枚举/域枚举有,复杂类型弱
序列SEQUENCE 独立对象,可多表共用/事务关联AUTO_INCREMENT 绑列
返回数据INSERT/UPDATE/DELETE ... RETURNING8.0 后部分 ROW 别名技巧,无标准 RETURNING
事务 DDLDDL 在事务内可回滚DDL 隐式提交,不可回滚
约束EXCLUDE 排他约束、CHECK(MySQL 8 才强制 CHECK)CHECK 早期只解析不执行
并发MVCC 实现不同,PG 需关注 vacuum/膨胀undo log 模型,回滚段自动

三、MySQL 也不是吃素的:迁移会失去什么

客观列一下 MySQL 的优势,这些都是迁移成本:

  1. 运维生态与人:DBA 市场保有量大,主从复制、半同步、MGR、binlog 备份恢复的资料和工具最多;PG 的 vacuum、复制槽、WAL 膨胀是新学习曲线。
  2. CDC 生态成熟:Canal、MaxWell 监听 binlog 的生态遍地都是;PG 逻辑复制(wal2json/Debezium)能用,但运维复杂度略高,复制槽积压会导致 WAL 磁盘爆掉。
  3. 简单高可用门槛低:主从切换工具链成熟;PG 的 Patroni + etcd(或 Consul)方案更强但组件更多。
  4. 类型宽容:MySQL 隐式转换方便(where phone = 138... 数字字符串不报错),PG 严格类型,老代码里大量"碰巧能跑"的 SQL 会直接报错——这是迁移返工的最大来源。
  5. 写入模型心智简单:没有 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_INCREMENTGENERATED ALWAYS AS IDENTITY(推荐)或 SERIAL低(DDL)
ON UPDATE CURRENT_TIMESTAMP无对应列属性,必须写触发器中(每个审计字段一个 trigger 函数)
INSERT ... ON DUPLICATE KEY UPDATEINSERT ... ON CONFLICT (...) DO UPDATE SET(更强)中(语义要逐条核对)
INSERT IGNOREON CONFLICT DO NOTHING低
REPLACE INTOON 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_CONCATSTRING_AGG(col, ',')低
DATE_FORMATto_char / to_date中(格式串完全不同:%Y→YYYY)
DATEDIFF / DATE_ADDdate 直接相减 / + interval '7 days'低
LIMIT m, nLIMIT 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-PlusCRUD 自动适配;手写 XML SQL、分页方言、${} 拼接要逐一审;MP 的 DbType 改 POSTGRE_SQL
JPA/Hibernate改方言即可,大部分自动适配(注意 GenerationType.IDENTITY 序列)
连接池HikariCP 无感;但 PG 连接开销大(每连接一个进程),生产建议前置 PgBouncer 事务级连接池
ShardingSphere / 分库分表确认版本对 PG 方言支持度
Canal / Debezium CDCCanal 不支持 PG;用 Debezium PG Connector(逻辑复制 + 复制槽)
备份/监控mysqldump→pg_dump;监控模板、Grafana Dashboard、告警规则全部重做
客户端/数据工具DBeaver 无感;自研数据订正脚本、BI 工具连接串全部要改
ORM 里的原生 SQL全库搜索 nativeQuery、@Select 注解、JdbcTemplate 字符串

4.4 评估 4:停机窗口与迁移方案

数据量推荐方案停机时间
< 10GBpgloader 一次性全量迁移,割接窗口停写分钟~十几分钟
10GB~500GBpgloader 全量 + 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?

手段说明
反向 CDCPG 开启逻辑解码,Debezium 把 PG 变更同步回 MySQL(提前搭好并演练)
保留双写割接后一段时间应用层双写(PG 为主、MySQL 异步追),任何问题切回 MySQL 不丢数据;成本高但最稳
MySQL 只读保留至少保留 7~30 天可快速启动的 MySQL 实例(不回收资源),而不是迁完立刻下线
明确回滚触发条件预先约定:错误率 > X、数据不一致 > Y 笔、核心功能不可用 Z 分钟——值班人有权直接回滚,不用层层审批
回滚演练割接前在预发完整演练一次"正向迁移+反向回滚",测的是脚本和时间,不是信心

建议的切流节奏:灰度 1% → 10% → 50% → 100%,每一档观察业务指标和慢查询/错误日志;回滚时按灰度逆序撤回。


五、迁移后最常踩的 5 个坑

  1. 标识符大小写:PG 对不加引号的标识符折叠为小写,建表写 CREATE TABLE Order 后实际叫 order,查询 SELECT * FROM Order 报错——规范就是全部小写+下划线,永不加引号。
  2. 严格类型:WHERE phone = 13800000000(bigint 列与 numeric 字面量侥幸,varchar 列直接报错)、MyBatis 传 String 给 int 参数全报错。迁移期在测试环境开启全量回归。
  3. GROUP BY 更严格:SELECT 里的非聚合列必须出现在 GROUP BY 中(MySQL 8 默认 ONLY_FULL_GROUP_BY 也是这规则,但历史代码有大量违规)。
  4. vacuum/膨胀没人管:迁移后只做备份不监控膨胀,三个月后表膨胀 3 倍性能暴跌——上线即配 autovacuum 参数 + 膨胀监控(pgstattuple)。
  5. 连接数打满:MySQL 几百连接轻松扛,PG 每连接一个进程,连接数过高上下文切换拖垮整机——应用侧 HikariCP 小池 + 全局 PgBouncer。

六、MySQL vs PG 语法差异速查表(可直接贴 wiki)

场景MySQLPostgreSQL
自增主键id BIGINT AUTO_INCREMENT PRIMARY KEYid BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
建表后缀ENGINE=InnoDB DEFAULT CHARSET=utf8mb4无
字符串转义/引号`col`"col"(建议不加引号,小写下划线)
布尔TINYINT(1),0/1boolean,true/false
取自增 IDlast_insert_id() / useGeneratedKeysRETURNING id / useGeneratedKeys
UpsertON DUPLICATE KEY UPDATE c=VALUES(c)ON CONFLICT(uk) DO UPDATE SET c=EXCLUDED.c
忽略冲突INSERT IGNOREON CONFLICT DO NOTHING
删后重插REPLACE INTOON CONFLICT DO UPDATE(注意语义)
限制更新行数UPDATE t SET ... LIMIT 100WHERE 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 = 0WHERE 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_logpg_stat_statements 扩展
当前库函数库DATABASE()current_database()
字符长度CHAR_LENGTH/LENGTHlength(char_length)
建索引(在线)ALGORITHM=INPLACE, LOCK=NONECREATE INDEX CONCURRENTLY(不能在事务里)
模糊/正则LIKE / REGEXPLIKE / ~(~* 忽略大小写)
注释-- 或 #--(要求后跟空格),/* */
事务 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 的经历吗?最痛的差异是哪一个?是整体迁还是两库共存?评论区聊聊。


参考资料


标题:为什么从 MySQL 迁移到 PostgreSQL?——功能对比+迁移前的 5 个评估
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/22/1789825533465.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消