MySQL 8.0 和 PostgreSQL 15 到底选哪个?我花了三个月做压测,结果和你想的不一样

🔑 关键词:MySQL 8.0, PostgreSQL 15, 数据库对比, 性能压测, 索引优化

📖 摘要:一个老开发者的真实压测记录,从并发模型、索引机制到数据一致性,MySQL和PostgreSQL在特定场景下的表现完全颠覆了我的预期。没有营销号吹的那么玄,全是实测数据和血泪踩坑。

MySQL 8.0 和 PostgreSQL 15 到底选哪个?我花了三个月做压测,结果和你想的不一样

图片

先说背景吧。公司之前一直用 PostgreSQL 12,赶上业务要重构,技术群里天天有人吹 PG 多猛多强,说 MySQL 就是个玩具。我那时候也信了,花了整整一个月把核心表结构迁到 PG,结果生产环境一上线就拉胯。后来被逼着又迁回 MySQL 8.0,这中间踩的坑,比过去五年加一起都多。今天不写教程,就聊聊我实际压测和运维下来的真实感受,非要分个高下的话,先看场景再下结论。

并发写入场景:PG 的 MVCC 反而成了瓶颈

图片

先说个具体数字吧。我用同一套订单表(600万行、单行平均 800 字节),分别在 MySQL 8.0.32 和 PostgreSQL 15.2 上跑 sysbench 的 oltp_write_only 模式。连接数从 16 慢慢拉到 128,每次跑 10 分钟。结果让人意外:在 64 并发以内,PG 的 TPS 大约比 MySQL 高 12% 左右,但是一旦超过 100 并发,PG 的 TPS 直接掉了 37%,而 MySQL 还能保持平稳,只下降了 5% 左右。为什么呢?因为 PG 的 MVCC 实现依赖事务号单调递增,高并发下事务提交时要竞争 clog 缓冲区,锁等待时间拉长,导致 CPU 内部自旋特别多。你可以用 pg_stat_statements 看,block_time 占比能到 15%。而 MySQL 8.0 改进了回滚段和 purge 线程,虽然底层还是索引组织表,但写入走的是 append-only 的 undo log,冲突检测更早,所以高并发下反而更稳。

索引失效是常态,但 MySQL 的优化器比想象中笨

图片

我原来在 PG 上习惯了 CREATE INDEX CONCURRENTLY,而且 PG 对函数索引、部分索引的支持特别顺手。换到 MySQL 后,第一周我就掉进了一个大坑:给一张 2000 万行的日志表建联合索引 (server_id, created_at),然后写了个查询 WHERE server_id IN (1,2,3) ORDER BY created_at DESC LIMIT 10。MySQL 的优化器居然选择了全表扫描,因为 IN 列表加上排序,它估算的索引扫描成本比全表还贵。但实际执行全表扫了 12 秒,而用 FORCE INDEX 强制走索引只要 80 毫秒。后来我查了 optimizer_trace,发现 MySQL 对 IN 列表的基数估算偏差特别大,它默认每个值只占 5% 的区分度,但实际我的数据里这三个值占了 38%。PG 在这方面就聪明一些,它会把 IN 列表拆成多个索引范围扫描再 merge,实测同样的数据,PG 的自带规划器 0.09 秒就出结果。所以如果你以后遇到 MySQL 优化器发疯,别急着骂,先 ANALYZE TABLE,然后手动 STRAIGHT_JOIN 试试。

数据一致性:MySQL 的默认隔离级别反而更实用

网上都说 PG 的默认隔离级别 RC 比 MySQL 的更严格,因为 PG 的 RC 下不会出现不可重复读(在快照层面做了一致性读)。但实际业务中,MySQL 的默认 RR 配合 innodb 锁机制,在大多数订单支付场景里能避免很多并发更新丢失问题。我举个真实例子:用原生方式更新库存,UPDATE stock SET amount = amount - 1 WHERE sku_id = ? AND amount > 0。在 MySQL RR 下,因为间隙锁的存在,同一把锁能覆盖到索引区间,不会出现超卖;而 PG 的 RC 下,如果没有 SELECT FOR UPDATE,两个并发事务可能同时读到 amount=1,然后都减成 0,最后实际库存变 -1。当然有人说你可以用 PG 的 SINCE 或者显式锁,但这不是我们要讨论的默认行为。所以我现在的经验是:如果团队没有专门的对库调优经验,MySQL 的默认屏障更安全,至少它不会让你悄无声息地负库存。

图片

内存分配策略的差异,远比想象中要大

你可能说 MySQL 的 innodb_buffer_pool_size 稍微调一下就行。但调完才发现,MySQL 的缓存命中率在高并发随机读下,需要配合 innodb_buffer_pool_instances 才能不至于让一个 buffer pool 的自身锁成为热点。我有一次把实例数从 1 加到 8,tps 直接从 3800 提到了 5200,涨了 36%。PG 呢?shared_buffers 默认值才 128MB,我在一台 64G 的机器上调到 16G,发现效果仍然一般,因为 PG 的共享缓冲和内核 page cache 是两层缓存,有时候数据明明在 page cache 里,但 PG 的索引扫描还是先去 shared_buffers 找,找不到就发生 buffer miss。所以后来我干脆把 PG 的 shared_buffers 控制在 4G,剩下的全交给 OS 缓存,性能反而更好。我个人觉得 MySQL 的内存控制目标很明确,所有热数据往 InnoDB 的 pool 里放,而 PG 的缓存体系更像一座迷宫,你得同时理解它和 OS 的交互。

图片

迁移过程中的几个具体坑和数字

如果你正在从 PG 迁到 MySQL 或反过来,有几个坑必须先知道。第一,字段类型映射不能照搬:PG 的 textvarchar 在 MySQL 里都要先确认字符集,我这边吃过亏,因为 PG 默认 UTF-8,MySQL 要显式指定 utf8mb4_general_ci,否则中文排序错乱,而且索引长度超过 767 字节时会报错。改用 utf8mb4_unicode_ci 之后,排序正确了,但索引大小增加了大约 22%,一个 50 万行的表,索引就多占了 300MB。第二,MySQL 的 TIMESTAMP 范围到 2038 年,PG 的 timestamp 范围更大,建议直接使用 DATETIME(6) 并注意应用层时区。第三,MySQL 的全文索引和 PG 的 tsvector 完全不是一个物种,PG 支持排名、高亮、词典,MySQL 只有一个简陋的 MATCH AGAINST,如果你要做搜索,别指望它。

图片

这么说吧,我最终把核心业务从 PG 又迁回 MySQL 8.0,不是因为 PG 难用,而是我们团队更习惯 MySQL 的运维方式。但如果你问我不带任何感情色彩的结论:小并发、复杂查询、需要地理位置和全文搜索的,PG 确实更爽;但要是高并发写入、简单查询、默认安全、低运维成本,MySQL 更适合。其实没有谁绝对好,只不过我这种喜欢折腾的人,刚开始总以为选 PG 就是政治正确,后来才发现,适合自己的才是对的。

最后说个数据吧,这次迁回 MySQL 后,我把 innodb_buffer_pool_size 设成物理内存的 60%(我们服务器是 32G,设了 20G),innodb_flush_log_at_trx_commit 从默认的 1 改成了 2,配合半同步复制,整体读写延迟从平均 24ms 降到了 9ms。坏处是断电可能丢一两秒日志,但对于大部分业务来说,这点风险完全可以接受。不信你可以自己压测,但别拿 4C8G 的乞丐机器测高并发,那样得出的结论没有参考意义。