主题
PgSQL vs MySQL:两条存储引擎路线,怎么选
一句话:两者都能做 OLTP,差异在存储引擎路线——PostgreSQL 是堆表 + 堆内多版本 + 扩展,MySQL InnoDB 是聚簇索引 + undo + 成熟生态;选型看工作负载和团队约束,不是排名。
适用 PostgreSQL 16/17、MySQL 8.0/8.4。版本不同,部分能力会变,以官方文档为准。
为什么需要
「选 PgSQL 还是 MySQL」通常不是一个纯技术问题。已有团队、云厂商 RDS、ORM 生态、运维习惯、迁移成本,都会影响结论。真正要回答的是:当前业务疼在哪里,换库能不能解决,代价是否值得。
把两者当成「谁更强」很容易得出错误结论:同一个 workload,换库后可能更快,也可能更慢;差异往往来自索引结构、事务实现和连接模型,而不是某个功能有没有。
核心机制
表与索引的组织方式
- PostgreSQL:堆表(heap)存储行,主键是普通唯一 B-tree 索引;索引项指向行的物理位置(ctid)。二级索引查到 ctid 后,还要回堆取行。
- MySQL InnoDB:表按主键组织成聚簇索引(clustered index),行数据就在主键 B-tree 的叶子节点;二级索引叶子存主键值,查到后还要按主键再查一次聚簇索引(除非覆盖索引)。
这带来两个直接后果:
- 按主键点查,InnoDB 通常少一次回表;按二级索引查非覆盖列,InnoDB 多一次主键查找。
- PostgreSQL 的
UPDATE会写新行版本;如果索引列没变且页内有空间,走 HOT 更新,不新增索引项;否则要新增索引项。InnoDB 多数更新在原页完成,旧版本放 undo。
MVCC 与清理
| 维度 | PostgreSQL | MySQL InnoDB |
|---|---|---|
| 旧版本位置 | 堆表内的完整行版本 | undo 表空间中的变更差异 |
| 版本链 | 行头 xmin/xmax + ctid/HOT 链 | undo 链表 + DB_ROLL_PTR |
| 清理 | Vacuum 回收死元组、冻结 XID | Purge 线程清理 undo |
| 长事务代价 | 表膨胀、Vacuum 追不上 | undo 膨胀、回滚段增长 |
| 默认隔离级别 | Read Committed | Repeatable Read |
两者都要为长事务付出代价,只是代价放在不同地方:PostgreSQL 放在主表空间和维护任务,MySQL 放在 undo 表空间和回滚段。
日志与恢复
PostgreSQL 的 WAL 同时承担崩溃恢复、物理复制和逻辑解码;PITR 依赖基础备份 + WAL 归档。
MySQL 有三类日志:redo log 负责崩溃恢复,undo log 负责 MVCC/回滚,binlog 负责复制和 PITR。选型时要问清楚:备份恢复链路、复制方式、故障切换工具,分别对应哪套日志。
连接模型
PostgreSQL 每个连接一个后端进程;MySQL 每个连接一个线程。两者在高并发下都需要连接池,但 PostgreSQL 的每连接内存和进程切换成本更高,通常更依赖 PgBouncer 这类外部池。不要把它简化成「MySQL 不需要连接池」。
能力差异
| 能力 | PostgreSQL | MySQL 8.0/8.4 |
|---|---|---|
| JSON | jsonb + GIN/表达式索引 | JSON 函数 + 生成列/多值索引,无 GIN 等价物 |
| 全文检索 | tsvector + GIN,中文需扩展 | FULLTEXT,中文分词依赖 ngram 等 |
| 地理空间 | PostGIS | 空间类型 + R-tree |
| 向量 | pgvector 扩展 | 无内置,需外部向量库 |
| 部分索引 | 支持 | 不支持 |
| 表达式索引 | 支持 | 函数索引(8.0.13+) |
| 并行查询 | 较成熟 | 8.0.18+ 有 hash join,并行能力有限 |
| 扩展 | 类型/操作符/索引/FDW/扩展 | 插件/组件,内核扩展空间较小 |
| 分区 | 声明式分区 | 原生分区,限制较多 |
MySQL 8.0 已经补上 CTE、窗口函数、CHECK 约束、函数索引等能力,不能用「MySQL 不支持复杂 SQL」这类旧结论做选型。
最小上手:用同一 workload 量化
先在同一台机器、同一份数据、相同持久化设置下压测,不要跨环境比数字。
bash
# PostgreSQL:初始化 100 倍数据,跑 select-only 和读写混合
pgbench -i -s 100 appdb
pgbench -c 64 -j 8 -T 300 -S appdb # 只读点查
pgbench -c 64 -j 8 -T 300 appdb # TPC-B 类读写混合bash
# MySQL:point select / write only / read write
sysbench oltp_point_select \
--tables=10 --table-size=1000000 --threads=64 --time=300 run
sysbench oltp_write_only \
--tables=10 --table-size=1000000 --threads=64 --time=300 run
sysbench oltp_read_write \
--tables=10 --table-size=1000000 --threads=64 --time=300 run压测时至少记录:
| 指标 | 为什么看 |
|---|---|
| QPS/TPS | 吞吐 |
| p50/p95/p99 延迟 | 平均值会掩盖尾延迟 |
| CPU、IOPS、fsync 延迟 | 瓶颈在 CPU、磁盘还是日志 |
| 每行写入的 WAL/binlog 字节数 | 写放大和复制成本 |
| 缓存命中率 | 数据是否装得下内存 |
| 复制延迟 | 高写入下备库能不能追上 |
pgbench 默认输出平均/最大延迟,p99 需要开 --log 后自己统计,或用其他压测工具;sysbench 会输出 percentile。两边的 durable 设置要一致,否则比较没有意义。
生产动作
- 先定位瓶颈再选型:慢在 SQL、索引、锁、连接还是容量?换库不能修复一个没建索引的查询。
- 迁移前做计划对比:把核心查询分别在两边
EXPLAIN/EXPLAIN ANALYZE,看扫描行数、回表次数、排序是否落盘,而不是只看总耗时。 - 写放大要实测:PostgreSQL 看 WAL 生成速率(
pg_stat_wal,PostgreSQL 14+);MySQL 看 binlog 和 redo 的写入量。高写入场景下,这决定了磁盘和复制带宽。 - 连接池两边都要:PostgreSQL 用 PgBouncer 的 transaction 模式;MySQL 用 ProxySQL 或驱动侧池。按每连接实际内存估算,不要拍一个固定值。
- Vacuum/Purge 都要监控:PostgreSQL 看
n_dead_tup和age(datfrozenxid);MySQL 看 history list length 和 undo 表空间。 - 复制与切换方案要一起选:PostgreSQL 常见 Patroni + etcd;MySQL 常见 InnoDB Cluster/Group Replication 或 MHA/Orchestrator。数据库选型包含运维工具链。
边界
- 没有通用赢家:简单主键点查、读多写少、团队和云生态成熟,MySQL 往往更省事;复杂查询、JSON + 关系混合、地理/时序/向量扩展,PostgreSQL 更合适。
- 不要为了「扩展性」迁移:如果当前没有用上 JSONB、PostGIS、pgvector、FDW 等能力,扩展性只是潜在收益,不足以覆盖迁移成本。
- MySQL 8.0 不是旧 MySQL:CTE、窗口函数、CHECK、函数索引都已具备;用 5.7 的印象做判断会失真。
- PostgreSQL 不是「不会膨胀」:堆内多版本决定了它必须认真做 Vacuum;长事务、批量更新、autovacuum 配置不当都会膨胀。
- 压测数字不可直接复用:硬件、数据分布、并发模型、持久化设置不同,结论会反过来;以自己 workload 的实测为准。
- 许可证与生态:PostgreSQL License 宽松;MySQL 是 GPL,Oracle 持有商标。云厂商的托管版本、备份、监控、支持能力也要计入。
讲清楚
PostgreSQL 和 MySQL 的差异,底层是两条存储引擎路线。PostgreSQL 用堆表存行、堆内多版本做 MVCC,所以读旧版本不用拼 undo,但必须 Vacuum,长事务会让表膨胀;InnoDB 用聚簇索引组织表、undo 存旧版本,主键点查少一次回表,但二级索引查非覆盖列要多一次主键查找,长事务会让 undo 膨胀。日志也不同:PostgreSQL 的 WAL 同时管恢复和复制,MySQL 的 redo/undo/binlog 分工更细。选型不要问「谁更强」,要问 workload:主键点查多、团队和云生态在 MySQL,就选 MySQL;复杂查询、JSON + 关系混合、地理/时序/向量,PostgreSQL 更省事。最后用同一套数据和压测脚本量 QPS、p99、写放大和复制延迟,数字比标签可靠。