PostgreSQL 18 vs 17.9:2000 万行实测,大聚合慢 1.6 倍,skip scan 是唯一稳赢项
先说前提:所有数据来自单机云盘环境(Docker,8 核 / 7.6GB 内存,普通云盘)。异步 I/O 的效果强依赖存储,换成快的本地 NVMe 或调优过的机器,下面关于 I/O 的结论可能变化。
升级本站之前,把同样的 2000 万行分别灌进 PostgreSQL 17.9 和 18,跑了五类真实系统里常见的表和查询。基准代码(docker-compose、数据生成、压测、图表)在作者 GitHub 上。
结论表
| 场景 | PostgreSQL 17.9 | PostgreSQL 18 | 结果 |
|---|---|---|---|
| 业务表过滤(skip scan) | 847ms | 585ms | 更快 |
| 销售/日志聚合报表(异步 I/O) | 4.3s | 7.1s | 反而更慢 |
| 深多表 join | 1.7s | 2.3s → 1.1s | 看配置 |
| 登录(按 email 查单个用户) | <1ms | <1ms | 无变化 |
| 文章模糊搜索(部分匹配) | 8.2ms | 8.1ms | 无变化 |
「升到 18 就全面变快」这件事没有成立。release notes 里的「faster」是有条件的,条件是什么表、什么查询、快多少,得自己测出来,否则没法为停生产升级找到理由。
测法
按一个业务 SaaS 建模(项目管理 + 活动日志分析 + 内部文章库),共七张表:organizations、users、projects、activity log、articles 等。
数据用「种子池放大」的方式造。纯空壳假数据测不出真实搜索的重量,但用 Faker 一行行生成 2000 万行太慢。所以先用 Faker 生成一次真实的种子池(几万个姓名、公司名和句子),再通过 COPY 组合放大成大表。随机种子固定,17.9 和 18 拿到的数据完全一致,隔离出版本差异。
# 用 Faker 生成一次种子池,再用 COPY 放大
names = [fake.name() for _ in range(50000)] # 真实姓名
sentences = [fake.text() for _ in range(5000)] # 文章正文素材
# 2000 万行活动日志通过采样这个池子由 COPY 流式写入
shared_buffers=256MB,让读取真的落到磁盘上。activity log 表 2000 万行约 2.9GB。每条查询用 EXPLAIN (ANALYZE, BUFFERS) 跑 5 次取中位数。看的是人实际等了几秒,不是「快了 40%」这种百分比。
大聚合:18 反而慢了
这是仪表盘式查询,按月、按地区聚合销售或访问日志。整表扫、不走索引,本该是 18 的异步 I/O 最该发挥作用的地方。异步 I/O 的意思是「让磁盘一次读多个块,等待期间去干别的活」。18 新增了 io_method 参数,默认 worker(专用进程批量预取)。
结果相反。同一条聚合,17.9 是 4.3s,18 默认配置是 7.1s,慢了 1.6 倍,五次跑下来稳定复现,不是噪声。
| 大聚合(2000 万行) | 耗时 |
|---|---|
| PostgreSQL 17.9 | 4.3s |
PostgreSQL 18,io_method=worker(默认) | 7.1s |
PostgreSQL 18,io_method=sync | 6.2s |
PostgreSQL 18,io_method=io_uring | 6.1s |
把 io_method 切到 io_uring,从 7.1s 降到 6.1s,但依然没追上 17.9 的 4.3s。这条聚合查询在这个环境里,18 就是更慢。
再重复一次前提:这是普通云盘上的结果。如果你的业务重度依赖大聚合,不要默认「18 有异步 I/O,聚合就会更快」,升级前在自己的机器上测。
skip scan:唯一明确赢的地方
典型的多租户 SaaS 查询:按时间范围过滤 activity log 表。表上有复合索引 (type, timestamp),type 有 8 个取值(view/click 之类)。但查询只按 timestamp 过滤时,17 用不好这个索引,因为前导列 type 没出现在条件里,只能退化为顺序扫描。18 的 skip scan 能在索引内部跳过 type 的 8 个取值,仍然用上 timestamp 那一段。
同样的 SQL、同样的数据、同样的索引,执行计划完全变了:
-- PostgreSQL 17.9:前导列缺失,退化为全表扫描
Parallel Seq Scan on events -- 读取约 370k blocks
-- PostgreSQL 18:skip scan 走索引
Bitmap Index Scan on idx_events_type_time -- 约 150k blocks
Index Searches: 17 -- 分多轮探测索引(skip scan)
847ms → 585ms,磁盘块从约 370k 降到约 150k。
关键在于不需要重建任何索引。应用代码、表定义、索引都不动,只是升级一版,原来走全表扫的查询就开始走索引了。大多数公司业务表上「顺手加的复合索引」正好是最大受益者。过滤返回的行越少,收益越大:在 1000 万行上收窄时间范围,同一条查询从 34ms 降到 5.6ms,约 6 倍。这里 2000 万行匹配约 19 万行,差距就小一些。收益取决于查询返回多少行,以及前导列有多少个不同值——前导列有几百万个 distinct 值时,要跳过的轮次变多,收益随之下降。
深 join:默认慢,改一个设置就快
| 深 join | 耗时 |
|---|---|
| PostgreSQL 17.9 | 1.7s |
PostgreSQL 18,worker(默认) | 2.3s(慢于 17) |
PostgreSQL 18,io_uring | 1.1s(快于 17) |
同一个 18,只改一个 I/O 设置,就把「更慢」翻成「更快」。所以升级到 18 后,先在自己的负载上对比 io_method,默认的 worker 不一定是最优。Linux 新内核上值得试 io_uring。
完全没变的部分
登录时的点查在 17 和 18 上都是瞬时的,1ms 以内。文章模糊搜索 8.2ms → 8.1ms。知道「以鉴权、主数据查询这类轻查询为主的系统,在 18 上体感不会有差别」本身就有助于升级决策——快慢波动只出现在一次读大表的重查询上。
升级当天的坑:统计信息
PostgreSQL 靠数据量和分布统计来选执行计划。到 17 为止,pg_upgrade 不会把这些统计信息带过去。迁移刚完成那一刻,数据库「对自己的内容一无所知」,在你手动跑 ANALYZE 重新收集之前,它会选烂计划、跑得很慢。表越大,这次 ANALYZE 越久——2000 万行大约 1 秒,但几千万到几十亿行就要几分钟,这期间生产没恢复满速。
18 的 pg_upgrade 会把统计信息搬过去,迁移结束的第一条查询就用对计划,那个「为什么升级完什么都慢」的窗口降到零。
两个边界:扩展统计信息(extended statistics)不会被搬运;可以用 --no-statistics 关掉这个行为。
checksum 默认开、MD5 弃用与移除项
18 里新初始化的库默认开启 data checksums。实测确认:17.9 报 data_checksums = off,18 报 on。pg_upgrade 要求源和目标的 checksum 设置一致,所以从没开 checksum 的老集群升级需要处理,比如用 --no-data-checksums 初始化目标库。
MD5 密码在 18 上建用户会告警:
WARNING: setting an MD5-encrypted password
DETAIL: MD5 password support is deprecated and will be removed in a future release of PostgreSQL.
还在用 MD5 的,现在就该往 scram-sha-256 迁。
被移除的内容:
pg_stat_wal的部分列(wal_write/wal_sync等),读这些列的监控工具会挂- 对 1.1.1 之前版本 OpenSSL 的支持
- 编译选项
--disable-spinlocks/--disable-atomics,以及老 CPU 架构(HP-PA)
另外 uuidv7() 在 18 成为正式功能;generated columns 的默认值改成「不存储、读时计算」(VIRTUAL),迁移表定义时需要确认。
测试局限
- 存储差异:异步 I/O 的效果强依赖磁盘速度和类型,本文全部结论建立在单机云盘上
- 规模:上限 2000 万行,改
SCALE可以跑 5000 万或 1 亿+ - 未测写密集负载,只聚焦读查询
是否值得升级
不是全面变快,大聚合在这个环境里确实变慢了。结论仍然是「值得升」,三条理由:
- 只要业务表上有复合索引,skip scan 一项就够本。
- 运维和安全改进是一起到的:统计信息随迁移搬运、checksum 默认开启、MD5 弃用。
- 但升级后一定要在自己环境里测,尤其是依赖大聚合和 join 的系统,把
io_method的worker/sync/io_uring三档都对比一遍。
参考:PostgreSQL 18 release notes https://www.postgresql.org/docs/current/release-18.html 。完整基准代码在作者 GitHub 上,包含 docker-compose、数据生成、压测脚本和图表。方法:EXPLAIN (ANALYZE, BUFFERS),5 次取中位数,PostgreSQL 17.9 / 18,数据完全一致(固定种子),shared_buffers=256MB,7.6GB 内存,8 核。