编程 PostgreSQL 18 vs 17.9:2000 万行实测,大聚合慢 1.6 倍,skip scan 是唯一稳赢项

2026-10-18 20:01:53

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.9PostgreSQL 18结果
业务表过滤(skip scan)847ms585ms更快
销售/日志聚合报表(异步 I/O)4.3s7.1s反而更慢
深多表 join1.7s2.3s → 1.1s看配置
登录(按 email 查单个用户)<1ms<1ms无变化
文章模糊搜索(部分匹配)8.2ms8.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.94.3s
PostgreSQL 18,io_method=worker(默认)7.1s
PostgreSQL 18,io_method=sync6.2s
PostgreSQL 18,io_method=io_uring6.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.91.7s
PostgreSQL 18,worker(默认)2.3s(慢于 17)
PostgreSQL 18,io_uring1.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 亿+
  • 未测写密集负载,只聚焦读查询

是否值得升级

不是全面变快,大聚合在这个环境里确实变慢了。结论仍然是「值得升」,三条理由:

  1. 只要业务表上有复合索引,skip scan 一项就够本。
  2. 运维和安全改进是一起到的:统计信息随迁移搬运、checksum 默认开启、MD5 弃用。
  3. 但升级后一定要在自己环境里测,尤其是依赖大聚合和 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 核。

推荐文章

程序员茄子在线接单