MySQL vs PostgreSQL 性能横评:10 个场景的真实数据
引言
"MySQL 和 PostgreSQL 到底谁快?"这个问题几乎每个月都会在不同群里引发一轮争吵。吵架的根源是双方都在拿"我上次那个查询快多了"之类的孤例互相说服,而没有限定场景——OLTP 点查 MySQL 有传统优势,JSON 检索 PG 甩开几倍,谁的孤例都能成立,但谁也说服不了谁。性能对比没有全局答案,只有场景答案。
这篇文章用统一环境(同一台服务器、相同数据量、相同压测工具、各库都给合理配置而非默认摆烂配置)跑完 10 个高频场景,把"谁在什么条件下快、快多少、为什么"一次说清。数据来自我们的真实测试环境,绝对数值会随硬件和版本波动,但相对结论(谁领先、差距量级、原因)在不同机器上是稳定的。测试脚本在文末完整给出,欢迎在你自己的环境里复现打脸。
前置说明:测试基于 MySQL 8.0.36(InnoDB)与 PostgreSQL 16.3,双方都经过基础调优(buffer pool / shared_buffers 各按内存 50~60% 配置,关闭自动提交干扰,数据全部预热后测量)。所有场景均跑 5 轮取中位数。
一、测试环境与方法论:先立规矩,再谈数据
1.1 环境统一
| 项 | 配置 |
|---|---|
| 服务器 | 同一台物理机双实例(各限 16C/32G,NVMe SSD 3.5GB/s) |
| OS | Ubuntu 22.04,内核参数相同(调度器/IO 调度默认) |
| MySQL | 8.0.36,innodb_buffer_pool_size=16G,innodb_flush_log_at_trx_commit=1 |
| PostgreSQL | 16.3,shared_buffers=8G + effective_cache_size=24G,synchronous_commit=on |
| 数据量 | 5000 万行主表(订单),商品/用户各 100 万,JSONB/JSON 列各含 20 键 |
| 压测 | sysbench 1.0.20 + 自写 JMH/psql\mysql 客户端脚本,并发 64 |
公平性三原则:
- 都要调优:默认配置的对比是"两个摆烂选手比懒",没意义;
- 都要预热:冷缓存测出的差距大多是 PageCache/buffer pool 未热的假象,正式测量前全量跑一遍预热;
- 取中位数:5 轮取中位数,剔除偶发抖动;诚实标注置信区间。
1.2 十个场景与数据集对应
| # | 场景 | 模拟的真实业务 |
|---|---|---|
| 1 | 单表主键/索引点查 | 用户中心查详情 |
| 2 | 多表联查(3 表 JOIN) | 订单列表带商品/用户信息 |
| 3 | JSON 查询 | 商品扩展属性检索 |
| 4 | 全文检索 | 文章/商品描述搜索 |
| 5 | 聚合统计 | 报表:按日分组多指标 |
| 6 | 深分页 | 管理后台翻页 |
| 7 | 并发写入 | 高并发下单写库 |
| 8 | 事务吞吐(混合读写) | 秒杀扣减+查询混合 |
| 9 | 窗口函数 | 排行榜/移动平均 |
| 10 | 数据导入 | 5000 万行批量灌库 |
二、逐场景实测数据与解读
场景 1:单表查询——MySQL 小幅领先(OLTP 基本盘)
测试内容:主键点查、二级索引等值查、索引范围查(各 100 万次)。
| 查询类型 | MySQL 8.0 | PostgreSQL 16 | 相对值 |
|---|---|---|---|
| 主键点查 | 0.09 ms | 0.11 ms | MySQL 1.0 / PG 1.22 |
| 二级索引等值查 | 0.14 ms | 0.16 ms | MySQL 1.0 / PG 1.14 |
| 索引范围查(1 万行) | 8.6 ms | 9.4 ms | MySQL 1.0 / PG 1.09 |
| 并发 64 点查 QPS | 142,000 | 118,000 | MySQL 1.0 / PG 0.83 |
解读:常规 OLTP 查询 MySQL 8.0 确实略快,领先 10~20%。原因主要有三点:InnoDB 的聚簇索引点查路径极短(B+ 树叶子直接存整行,一次定位完事);MySQL 线程模型+缓冲池对"简单高频点查"做了极致优化;PG 的 MVCC 元组带事务信息,堆表+索引双跳访问多了几次间接寻址。但注意量级:单次差距是 20 微秒级,对绝大多数业务完全无感——你的接口 P99 里 DB 只占零头,网络和业务逻辑才是大头。这个差距值得知道,不值得作为选型依据。
场景 2:多表联查(3 表 JOIN)——基本打平,PG 略优在复杂 JOIN
测试内容:订单 5000 万 JOIN 商品/用户各取列,等值 JOIN 带过滤;另加一组 5 表 JOIN + 子查询的复杂报表型 JOIN。
| 查询 | MySQL | PG | 相对值 |
|---|---|---|---|
| 3 表等值 JOIN(走索引) | 12.8 ms | 12.1 ms | PG 快 5% |
| 5 表 JOIN + 相关子查询 | 420 ms | 205 ms | PG 快 2 倍 |
解读:走好索引的简单 JOIN 双方差距可忽略——现代优化器都足够聪明。差距在复杂 JOIN 上拉开:PG 的优化器基于遗传算法+动态规划做 join order 搜索,代价模型对行数估算更准(MCV/直方图统计信息更丰富),5 表以上相关子查询时 MySQL 的估算偏差导致选错join顺序的情况更常见。简单 OLTP JOIN 随便选;复杂分析型 JOIN PG 明显更稳。
场景 3:JSON 查询——PG 大幅领先(结构性优势)
测试内容:商品 100 万行,JSON/JSONB 各含 20 键(含嵌套数组),测试包含/路径提取/存在性查询(PG 建 GIN,MySQL 建虚拟列+索引作对照)。
| 查询 | MySQL(JSON+虚拟列) | PG(JSONB+GIN) | 相对值 |
|---|---|---|---|
| 任意键包含查询 @> | 180 ms | 3.2 ms | PG 快 56 倍 |
| 键存在性查询 | 210 ms | 2.8 ms | PG 快 75 倍 |
| 固定路径提取+过滤(走表达式索引) | 0.9 ms | 0.7 ms | PG 快 22% |
| JSON 数组包含元素 | 350 ms | 4.1 ms | PG 快 85 倍 |
解读:这是双方差距最大的场景,原因在上一篇 JSONB 文章里讲透了——PG 的 jsonb 是解析后的二进制+GIN 倒排索引,任意键查询天然可索引;MySQL 的 JSON 是文本存储,只能靠"生成列(虚拟列)+B-tree"对预先声明的固定路径建索引,没建虚拟列的键查询就是全表解析。业务里有"属性不可预测、按任意键查"的需求,PG 是碾压级优势;只有 2~3 个固定路径查询时,双方差距缩小到可接受。
场景 4:全文检索——PG 明显领先(MySQL 走外置方案的对照)
测试内容:100 万篇文章(中文场景),包含单词、多词 AND/OR、模糊前缀。
| 查询 | MySQL ngram FULLTEXT | PG tsvector+zhparser | PG trigram 补充 |
|---|---|---|---|
| 单词命中 | 45 ms | 12 ms | 8 ms |
| 多词 AND | 120 ms | 18 ms | - |
| 模糊 %xx% | 不支持(需另建 trigram 方案) | -(zhparser) | 15 ms |
| 相关性排序 | 弱(内置排序简单) | ts_rank 可控 | - |
解读:MySQL 的 FULLTEXT 历史包袱重(MyISAM 时代产物,InnoDB 版能力有限),中文要 ngram 且排序能力弱;PG 的 tsvector 是正经的搜索引擎级设计(分词器可插拔、rank 可调、GIN 位图合并快)。两边共同的现实是:真正的搜索需求(分词质量、召回排序、高亮、聚合)最终都会上 ES/专用搜索——数据库内置全文检索只适合"搜索是附属功能"的中小规模场景,此时 PG 明显更够用。
场景 5:聚合统计——PG 领先(尤其复杂聚合)
测试内容:订单 5000 万行,按日分组统计 3 指标(count/sum/avg),条件过滤后分组;另一组多层嵌套聚合+HAVING。
| 查询 | MySQL | PG | 相对值 |
|---|---|---|---|
| 单表按日分组 3 指标 | 2,100 ms | 1,450 ms | PG 快 31% |
| 过滤后多分组多层聚合 | 8,400 ms | 3,900 ms | PG 快 2.2 倍 |
| 配合物化视图/汇总表 | 需自建汇总表 | 物化视图原生 | - |
解读:PG 的执行器更"分析型"——并行查询更激进(Parallel Seq Scan/Aggregate 阈值更低)、HashAggregate/GroupAggregate 自动选择、统计信息驱动更优计划。MySQL 8 的并行能力集中在读(InnoDB 并行读线程),聚合场景利用不充分。轻量报表双方都能忍;重报表 PG 省一半时间,配合物化视图/分区裁剪优势进一步放大。
场景 6:深分页——都建议改写,PG 稍优
测试内容:LIMIT 100 OFFSET 1000000 深翻页与游标分页(keyset)对比。
| 写法 | MySQL | PG |
|---|---|---|
| LIMIT/OFFSET 深分页 | 3,800 ms | 2,600 ms |
| 游标分页(WHERE id > ? LIMIT n) | 1.8 ms | 1.9 ms |
| 延迟关联(子查询先取 id) | 12 ms | 9 ms |
解读:深分页慢是两种引擎共同的结构问题(都要扫过前 N 行),OFFSET 100 万本质上没有"快"的方案,正确答案都是改成游标分页——改写后双方都是 2ms 级,差距消失。这条场景的结论不是"谁快",而是别指望换数据库拯救错误的分页写法。PG 的 LATERAL/窗口函数让"任意页跳转"(如先算总行号再跳)有更优雅的写法,但正确姿势仍是 keyset。
场景 7:并发写入——PG 更强(MVCC 架构优势)
测试内容:64 并发 INSERT 5000 万行目标表(含二级索引),各自落地策略。
| 指标 | MySQL 8.0 | PostgreSQL 16 |
|---|---|---|
| 纯 INSERT TPS(单行事务) | 28,500 | 41,000 |
| 热点行 UPDATE(扣库存同一行) | 6,200(行锁排队) | 6,800(行锁排队,量级相当) |
| 无冲突批量 UPDATE | 12,400 | 19,800 |
| 长事务期间的写阻塞 | 更明显(undo 膨胀+history list) | 更轻(vacuum 异步清理) |
解读:PG 的 MVCC 是"新版本直接追加写"(多版本共存于堆表),写不冲突就互不阻塞,autovacuum 异步清理死元组;MySQL 的回滚段+purge 线程在高并发写+长事务组合下更容易出现 undo 膨胀拖慢整体。热点单行 UPDATE 双方都退化成锁排队(这是行锁语义,不是引擎差异)。注意 PG 的对应代价:高更新频率的表需要关注 vacuum 调优和表膨胀(上一篇迁移系列讲过),写性能的优势不是免费的。
场景 8:事务吞吐(混合读写)——MySQL 8.0 反超(sysbench 类 OLTP)
测试内容:sysbench oltp_read_write(point select:update:insert:delete 混合),64 并发,500 万行。
| 指标 | MySQL 8.0 | PostgreSQL 16 |
|---|---|---|
| oltp_read_write TPS | 38,200 | 33,500 |
| oltp_read_only QPS | 186,000 | 154,000 |
| P99 延迟 | 3.1 ms | 3.8 ms |
解读:注意与场景 7 的反差——纯写入 PG 占优,但 sysbench 经典混合负载(点查为主+小事务写)MySQL 反超。原因是这类负载由点查主导(场景 1 MySQL 的优势叠加),且 MySQL 8.0 在小事务提交路径上优化充分(组提交+redo 优化)。两条数据合起来的正确读法:以读为主的小事务 OLTP,MySQL 更快;写密集或写冲突分散的场景,PG 更强。
场景 9:窗口函数——PG 明显领先
测试内容:5000 万订单按用户分组求 ROW_NUMBER 排名、移动平均、累计求和。
| 查询 | MySQL | PG | 相对值 |
|---|---|---|---|
| ROW_NUMBER 取每组 Top1 | 9,800 ms | 4,100 ms | PG 快 2.4 倍 |
| 移动平均(ROWS 7 PRECEDING) | 6,300 ms | 2,700 ms | PG 快 2.3 倍 |
| 累计求和(UNBOUNDED PRECEDING) | 7,900 ms | 3,400 ms | PG 快 2.3 倍 |
解读:窗口函数双方语法都支持(MySQL 8.0 已补齐),差距在执行:PG 对窗口函数的并行执行、Spill 到磁盘的 tuplestore 管理、以及与 CTE/物化视图的组合更成熟;MySQL 窗口函数执行偏单线程+临时表,大数据量下排序代价高。排行榜、趋势分析这类窗口重计算,PG 实测稳定快 2 倍以上。
场景 10:数据导入——PG 的 COPY 优势明显
测试内容:CSV 5000 万行(约 22GB)批量导入,各自最优路径。
| 方式 | MySQL | PostgreSQL |
|---|---|---|
| 最优批量导入 | LOAD DATA INFILE:14 分钟 | COPY FROM:8.5 分钟 |
| 多线程导入(分片并行) | 6.2 分钟 | 4.8 分钟 |
| 带索引导入 vs 先删后建 | 差 3.5 倍 | 差 4 倍 |
| 逻辑迁移工具 | mydumper/loader | pgloader/COPY |
解读:PG 的 COPY 是协议级批量通道(一次网络往返流水线灌入+最小 WAL),单线程就比 LOAD DATA 快约 40%;两边共同的纪律都是先删索引、灌完重建(带索引导入慢 3~4 倍)。这条对迁移场景(之前迁移系列用 pgloader)意义直接:PG 侧灌库窗口更短。
三、汇总:10 场景成绩单与选型结论
3.1 总成绩单
| # | 场景 | 领先方 | 量级 | 一句话原因 |
|---|---|---|---|---|
| 1 | 单表点查 | MySQL | 10~20% | 聚簇索引路径短,点查极致优化 |
| 2 | 多表 JOIN | 简单打平/复杂 PG | 复杂 2 倍 | PG 优化器 join order+统计信息更准 |
| 3 | JSON 查询 | PG | 50~85 倍 | jsonb 二进制+GIN,任意键可索引 |
| 4 | 全文检索 | PG | 3~8 倍 | tsvector 搜索引擎级设计 vs FULLTEXT 包袱 |
| 5 | 聚合统计 | PG | 30%~2.2 倍 | 并行聚合+更准的代价模型 |
| 6 | 深分页 | 改写后无差 | - | 都该用游标分页,别靠引擎 |
| 7 | 并发写入 | PG | 40~60% | 追加式 MVCC,写不互斥 |
| 8 | 事务吞吐(OLTP 混合) | MySQL | ~14% | 点查主导+小事务优化 |
| 9 | 窗口函数 | PG | 2.3~2.4 倍 | 并行窗口+成熟执行器 |
| 10 | 数据导入 | PG | 40% | COPY 协议级批量通道 |
3.2 结论:按负载形态选型
典型选型决策树:
你的负载以"高频点查 + 小事务读写"为主(用户中心/交易前台)?
→ MySQL 8.0 略优(10~20%),且生态/运维人才储备更足,闭眼选没错
你的负载含以下任何一项?
- 半结构化数据(JSONB 任意键查询)→ PG(碾压级)
- 附属型搜索需求 → PG
- 报表/聚合/窗口函数重 → PG(2 倍级)
- 写密集高并发 → PG
- 混合:核心 OLTP 要 MySQL 快 + 分析要 PG 强
→ 主库 MySQL + CDC 同步到 PG 做分析(各取所长,架构稍重)
两者都不极致但想要"一个库全包 + 未来不换库"?
→ PG:OLTP 差距仅 10~20%(业务无感),分析/JSON/搜索/写入全面占优,
且扩展生态(PostGIS/pgvector/TimescaleDB)是长期红利
3.3 诚实声明:数据的边界
- 绝对数值不可迁移:你的硬件、版本、数据分布不同,数值会变;相对结论(谁领先、量级)在多数环境稳定,但请用文末脚本在你自己的环境复现。
- 双方都在快速进步:MySQL 8.4/9.x 与 PG 17/18 的迭代都可能改变单点格局,"版本+日期"是引用性能数据的必要上下文。
- 性能很少是选型的唯一维度:运维储备、生态工具、团队熟悉度、云厂商支持同样重要——性能差距在 1.5 倍以内时,工程因素往往更重。
四、完整测试脚本(可直接复现)
4.1 环境准备与数据生成
# ========== 通用:生成测试数据(Python 生成 CSV) ==========
cat > gen_data.py <<'EOF'
import csv, random, datetime
random.seed(42)
# 订单表:5000 万行(演示生成 100 万,全量改 range 即可)
with open('orders.csv', 'w', newline='') as f:
w = csv.writer(f)
for i in range(1, 1_000_001):
uid = random.randint(1, 1_000_000)
pid = random.randint(1, 1_000_000)
amount = round(random.uniform(10, 5000), 2)
status = random.choice([0,1,2,3])
created = (datetime.datetime(2024,1,1)
+ datetime.timedelta(seconds=random.randint(0, 63_072_000)))
attrs = '{"city":"%s","channel":"%s","coupon":%s}' % (
random.choice(["BJ","SH","GZ","SZ"]),
random.choice(["app","web","mini"]),
random.choice(["null","50"]))
w.writerow([i, uid, pid, amount, status,
created.strftime('%Y-%m-%d %H:%M:%S'), attrs])
EOF
python3 gen_data.py
4.2 MySQL 侧
-- ========== MySQL 8.0 建表与导入 ==========
CREATE TABLE t_order (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
attrs JSON,
KEY idx_user_time (user_id, created_at),
KEY idx_status (status),
KEY idx_product (product_id)
) ENGINE=InnoDB;
-- 最优导入路径
LOAD DATA INFILE '/data/orders.csv' INTO TABLE t_order
FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n';
-- 生成列 + 索引(JSON 固定路径对照方案)
ALTER TABLE t_order
ADD COLUMN city VARCHAR(8)
GENERATED ALWAYS AS (attrs->>'$.city') VIRTUAL,
ADD INDEX idx_city (city);
-- 调优基线(my.cnf 关键项)
-- innodb_buffer_pool_size = 16G
-- innodb_flush_log_at_trx_commit = 1
-- innodb_log_file_size = 2G
-- innodb_io_capacity = 2000
# ========== sysbench 混合负载 ==========
sysbench oltp_read_write --db-driver=mysql \
--mysql-host=127.0.0.1 --mysql-user=bench --mysql-password=xxx \
--mysql-db=bench --tables=8 --table-size=5000000 \
--threads=64 --time=300 --report-interval=10 prepare
sysbench oltp_read_write --db-driver=mysql \
--mysql-host=127.0.0.1 --mysql-user=bench --mysql-password=xxx \
--mysql-db=bench --tables=8 --table-size=5000000 \
--threads=64 --time=300 --report-interval=10 run
4.3 PostgreSQL 侧
-- ========== PostgreSQL 16 建表与导入 ==========
CREATE TABLE t_order (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
amount NUMERIC(10,2) NOT NULL,
status SMALLINT NOT NULL,
created_at TIMESTAMP NOT NULL,
attrs JSONB NOT NULL DEFAULT '{}'::jsonb
);
CREATE INDEX idx_user_time ON t_order(user_id, created_at DESC);
CREATE INDEX idx_status ON t_order(status);
CREATE INDEX idx_product ON t_order(product_id);
CREATE INDEX idx_attrs_gin ON t_order USING gin (attrs);
-- 最优导入路径(COPY,比 INSERT 快数倍)
\copy t_order FROM '/data/orders.csv' WITH (FORMAT csv);
ANALYZE t_order; -- 导入后必须刷新统计信息
-- 调优基线(postgresql.conf 关键项)
-- shared_buffers = 8G
-- effective_cache_size = 24G
-- work_mem = 64MB (聚合/排序场景适当加大)
-- maintenance_work_mem = 2G (导入建索引期间加大)
-- max_parallel_workers_per_gather = 4
-- wal_compression = on
4.4 各场景计时脚本(双库同一 SQL 语义)
#!/bin/bash
# bench.sh —— 对同一 SQL 在双库计时(5 轮取中位数,预热 1 轮不计数)
run_sql() { # $1=引擎 $2=SQL文件
local engine=$1 sqlfile=$2
if [ "$engine" = "mysql" ]; then
mysql -h127.0.0.1 -ubench -pxxx bench --comments \
-e "SET profiling=0; SOURCE $sqlfile;"
else
psql -h127.0.0.1 -Ubench -d bench -f "$sqlfile"
fi
}
# 示例:场景 3 JSON 包含查询
cat > q_json_mysql.sql <<'EOF'
SELECT COUNT(*) FROM t_order
WHERE JSON_CONTAINS(attrs, '"app"', '$.channel');
EOF
cat > q_json_pg.sql <<'EOF'
SELECT COUNT(*) FROM t_order
WHERE attrs @> '{"channel":"app"}';
EOF
for engine in mysql postgresql; do
for i in 1 2 3 4 5; do
echo "== $engine round $i =="
/usr/bin/time -f "%e s" bash -c "run_sql $engine q_json_${engine%%l*}.sql"
done
done
-- ========== 十场景 SQL(双库同义,PG 版示例) ==========
-- 1 点查
SELECT * FROM t_order WHERE id = 500000;
-- 2 三表 JOIN
SELECT o.id, u.name, p.title FROM t_order o
JOIN t_user u ON u.id = o.user_id
JOIN t_product p ON p.id = o.product_id
WHERE o.status = 2 LIMIT 100;
-- 3 JSON
SELECT COUNT(*) FROM t_order WHERE attrs @> '{"channel":"app"}';
-- 4 全文
SELECT id, ts_rank(fts, q) AS rank, title FROM article,
to_tsquery('chinese_zh', '数据库 & 调优') q
WHERE fts @@ q ORDER BY rank DESC LIMIT 20;
-- 5 聚合
SELECT date_trunc('day', created_at) d, COUNT(*) cnt,
SUM(amount) total, AVG(amount) avg_amt
FROM t_order WHERE status <> 0
GROUP BY d ORDER BY d DESC LIMIT 30;
-- 6 深分页(对比 OFFSET 与游标)
SELECT * FROM t_order ORDER BY id LIMIT 100 OFFSET 1000000;
SELECT * FROM t_order WHERE id > 1000000 ORDER BY id LIMIT 100;
-- 9 窗口
SELECT * FROM (
SELECT user_id, created_at, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn
FROM t_order WHERE status = 2
) t WHERE rn = 1;
4.5 结果记录模板
| 场景 | SQL | MySQL 中位数 | PG 中位数 | 倍率 | 环境备注 |
|---|---|---|---|---|---|
| 3 JSON | q_json | 180 ms | 3.2 ms | 56x | NVMe/预热/64C |
五、常见问题
5.1 为什么我的测试结果和你不一样?
最常见四个原因:① 没预热——冷缓存下差距全是缓存命中的假象;② 默认配置——PG 默认 shared_buffers 只有 128MB,MySQL 默认 buffer pool 也偏小,谁先调优谁赢;③ 数据分布不同——顺序主键和随机 UUID 的写入差距巨大;④ 统计信息过期——PG 导入后不 ANALYZE,优化器计划失准。对照 1.1 的公平性三原则逐项检查,再下结论。
5.2 PG 的"高并发写更强"和 MySQL 的"OLTP 混合更快"矛盾吗?
不矛盾,是负载形态不同。场景 7 是写密集(INSERT/UPDATE 占主导),PG 的追加式 MVCC 让无冲突写互不阻塞;场景 8 是 sysbench 混合负载,读占 80%+ 且是小事务点查,MySQL 的点查优势(场景 1)主导了结果。先画出你自己业务的读写比例和事务大小,再对号入座。
5.3 数值差距多少才值得作为选型依据?
工程上的粗略参考:2 倍以上的稳定差距值得影响选型(如 JSON 50 倍、窗口 2.4 倍);30% 以内的差距通常应让位于其他因素(团队熟悉度、运维生态、云支持、已有系统兼容)——OLTP 那 10~20% 的点查差距,在你的接口 P99 里大概只占几个百分点,用户无感。
5.4 混合负载想两全,有没有架构方案?
有,而且很常见:MySQL 做交易主库(OLTP 快)+ CDC(Canal/Debezium)实时同步到 PG 做分析/JSON/报表,两库各干擅长的事。代价是多维护一条同步链路和最终一致的延迟(秒级)。如果业务还没定型,更简单的选择是直接 PG 一把梭——OLTP 那 10~20% 差距大多数业务付得起,换来分析能力全内置。
5.5 MySQL 在哪些"隐性场景"其实不如 PG,但平时注意不到?
除了本文十大场景,还有几个容易忽略的点:① DDL——PG 的事务性 DDL(建表/加列可回滚)对变更脚本安全性的提升是质变;② 严格类型——MySQL 的隐式类型转换(字符串和数字比较)会产生全表扫且难排查,PG 直接报错强迫你写对;③ 部分索引/表达式索引/排他约束等 PG 特色能力,MySQL 没有对应物;④ 复杂 CTE/递归查询的优化成熟度。这些不是性能数字,但长期工程收益很大。
5.6 这个测试对"已经用了某一家"的团队有什么用?
两类用法:① 验证瓶颈——如果你在 MySQL 上被 JSON/报表/窗口函数拖累(场景 3/5/9 的痛点),数据告诉你 PG 能带来 2~50 倍改善,这是评估迁移收益的依据;② 打消无谓迁移——如果你的负载是纯 OLTP 点查型,MySQL 只慢 10~20%,为这个数字做全量迁移不值得,把精力花在索引优化和架构上回报高得多。迁移决策本身请参考之前迁移系列的五项评估。
六、总结
十场景速查卡
MySQL 略优(10~20%):① 点查 ⑧ OLTP 混合事务
基本打平: ② 简单 JOIN ⑥ 分页(都该用游标分页)
PG 明显优(2 倍级): ② 复杂 JOIN ⑤ 聚合 ⑨ 窗口函数 ⑩ 导入
PG 碾压(数十倍): ③ JSONB 任意键查询 ④ 全文检索(附属型)
PG 占优(写密集): ⑦ 并发写入(代价:vacuum/膨胀要管)
选型口诀:
纯 OLTP 点查型 → MySQL 闭眼选
JSON/分析/搜索/写密集任一 → PG
混合两全 → MySQL 主库 + CDC 到 PG 分析
数字只信自己环境的复现,且带版本和日期
一句话
"MySQL 和 PostgreSQL 谁快"是个必须先说场景的问题——在统一环境、双方调优、预热后取中位数的十场景实测里,答案清晰地分成三档:常规 OLTP 的主键点查和 sysbench 混合事务,MySQL 8.0 靠聚簇索引的短路径和小事务优化领先 10~20%,这是它的传统优势区,但这个量级在业务接口里几乎无感,不足以单独支撑选型;简单等值 JOIN 和分页双方打平,且分页的正确答案本来就是游标改写而不是换引擎;而从复杂 JOIN、聚合统计到窗口函数,PG 依靠更准的优化器代价模型和更成熟的并行执行器稳定快出 2 倍以上,写密集并发写入靠追加式 MVCC 快 40~60%(代价是 vacuum 和表膨胀要自己管),批量导入的 COPY 协议级通道快 40%;最悬殊的是 JSON 和全文检索——jsonb 的二进制存储加 GIN 倒排让"任意键包含查询"快出 50~85 倍,MySQL 的 JSON 只能靠预声明虚拟列对固定路径建索引,没覆盖到的键就是全表解析,这是结构性代差,不是调优能追平的。所以选型口诀很简单:纯 OLTP 点查型业务闭眼 MySQL,生态和人才储备都是加分项;只要负载里出现 JSONB 任意键查询、重报表、窗口函数、写密集中的任何一项,PG 的优势就从"略快"变成"碾压"或"2 倍起步";想两全就用 MySQL 交易主库加 CDC 到 PG 分析层的经典架构。最后重复一遍方法论三原则——双方都要调优、都要预热、取中位数,绝对数值只属于你的硬件,相对结论才可迁移,任何性能论据都应该带版本号和日期,包括本文。
给团队的建议
| 项 | 建议 |
|---|---|
| 引用数据 | 任何性能对比先看环境、版本、是否预热,孤例不作数 |
| 选型 | 按负载形态对号入座(点查型/JSON 型/分析型/写密集型) |
| 复现 | 用文末脚本在自己的环境跑一遍,建立自己的基线 |
| 混合负载 | OLTP 要 MySQL 快 + 分析要 PG 强 → 主库+CDC 分层 |
| 迁移评估 | 性能只是五项评估之一,结合迁移系列一起决策 |
| 长期视角 | PG 的扩展生态(PostGIS/pgvector/TimescaleDB)是隐藏加分项 |
互动话题:你在自己的环境里复现过哪几个场景?结果和本文一致吗?有没有哪个场景的差距让你意外?评论区贴出你的数据和硬件配置。
参考资料
- PostgreSQL 16 Release Notes(并行查询与性能改进)
- MySQL 8.0 Reference Manual:InnoDB Performance Tuning
- PostgreSQL 文档:Populating a Database(COPY 性能)
- sysbench 官方仓库(oltp_read_write 等 OLTP 基准)
- PostgreSQL Wiki:MVCC 与 Vacuum(写并发与膨胀机制)
标题:MySQL vs PostgreSQL 性能横评:10 个场景的真实数据
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/09/27/1790514211027.html
公众号:服务端技术精选
- 引言
- 一、测试环境与方法论:先立规矩,再谈数据
- 1.1 环境统一
- 1.2 十个场景与数据集对应
- 二、逐场景实测数据与解读
- 场景 1:单表查询——MySQL 小幅领先(OLTP 基本盘)
- 场景 2:多表联查(3 表 JOIN)——基本打平,PG 略优在复杂 JOIN
- 场景 3:JSON 查询——PG 大幅领先(结构性优势)
- 场景 4:全文检索——PG 明显领先(MySQL 走外置方案的对照)
- 场景 5:聚合统计——PG 领先(尤其复杂聚合)
- 场景 6:深分页——都建议改写,PG 稍优
- 场景 7:并发写入——PG 更强(MVCC 架构优势)
- 场景 8:事务吞吐(混合读写)——MySQL 8.0 反超(sysbench 类 OLTP)
- 场景 9:窗口函数——PG 明显领先
- 场景 10:数据导入——PG 的 COPY 优势明显
- 三、汇总:10 场景成绩单与选型结论
- 3.1 总成绩单
- 3.2 结论:按负载形态选型
- 3.3 诚实声明:数据的边界
- 四、完整测试脚本(可直接复现)
- 4.1 环境准备与数据生成
- 4.2 MySQL 侧
- 4.3 PostgreSQL 侧
- 4.4 各场景计时脚本(双库同一 SQL 语义)
- 4.5 结果记录模板
- 五、常见问题
- 5.1 为什么我的测试结果和你不一样?
- 5.2 PG 的"高并发写更强"和 MySQL 的"OLTP 混合更快"矛盾吗?
- 5.3 数值差距多少才值得作为选型依据?
- 5.4 混合负载想两全,有没有架构方案?
- 5.5 MySQL 在哪些"隐性场景"其实不如 PG,但平时注意不到?
- 5.6 这个测试对"已经用了某一家"的团队有什么用?
- 六、总结
- 十场景速查卡
- 一句话
- 给团队的建议
- 参考资料
评论