PG18:核心新特性总览
PostgreSQL 18 于 2025 年 9 月 25 日正式发布,是本教程覆盖的第三个大版本(继 PG16、PG17 之后),也是整个十八阶段教程的收官专题。如果说 PG16 的关键词是「逻辑复制的工程化」、PG17 的关键词是「VACUUM 与复制槽的运维化」,那么 PG18 的关键词可以概括为两个字:架构——异步 I/O 子系统重写了内核与存储之间的交互方式,虚拟生成列改变了「值何时存在」的定义,OAuth 把企业级认证协议直接搬进了 pg_hba.conf。
除了这些大架构改动,PG18 在「日用的爽感」上也非常慷慨:EXPLAIN ANALYZE 默认带上了 BUFFERS 输出、多列 B-tree 索引终于支持跳过扫描(skip scan)、uuidv7() 提供了时间有序的主键、pg_upgrade 升级后不再丢优化器统计、逻辑复制槽可以设置空闲超时自动失效。这些改动单看每一个都不大,合在一起却显著降低了「把 PostgreSQL 用对」的门槛。
本页是 PG18 系列三页中的总览页:先把全部主要特性按「是什么、解决什么、怎么用最小例子跑起来」过一遍,下一页再聚焦云原生与部署,最后一页回到生态与展望。与前面 PG16、PG17 两章一样,本页所有特性均以官方 release notes 为准,不收录任何「预期」「传闻」中的功能。
前置知识
建议先阅读:逻辑复制槽与故障转移(PG17)
基础概念
版本基本信息
| 项目 | 内容 |
|---|---|
| 版本号 | PostgreSQL 18 |
| 正式发布日期 | 2025-09-25(官方 release notes 标注) |
| 支持周期 | 按官方版本政策,每个大版本支持 5 年(PG18 预计支持到 2028 年) |
| 升级方式 | pg_upgrade、pg_dumpall 逻辑导出、逻辑复制三种(与既往一致) |
| 协议版本 | 线协议(wire protocol)3.2(PG18 新增,取消请求密钥提升为 256 位) |
| 默认行为变化 | initdb 默认启用数据校验和;生成列默认 VIRTUAL |
PG18 特性全景
先给一张「全景图」。这张表贯穿 PG18 系列三页,本页讲核心特性,后两页展开部署与生态。所有条目均新增于 PG18,出处为官方 release notes。
| 分类 | 特性 | 一句话概括 |
|---|---|---|
| I/O | 异步 I/O 子系统 | io_method 选择 worker / io_uring / sync,顺序扫描、位图堆扫描、VACUUM 等受益 |
| 表设计 | 虚拟生成列 | GENERATED ALWAYS AS 默认 VIRTUAL,读取时计算、不落盘 |
| 主键设计 | uuidv7() | 时间有序 UUID,索引局部性远好于 gen_random_uuid() |
| 认证 | OAuth 2.0 | pg_hba.conf 新增 oauth 认证方法,配合 oauth_validator_libraries |
| 索引 | B-tree 跳过扫描 | 多列 B-tree 索引在前导列无等值条件时也能被使用 |
| 索引 | GIN 并行构建 | GIN 索引创建可以走并行 worker |
| SQL | RETURNING OLD/NEW | INSERT / UPDATE / DELETE / MERGE 可显式返回旧值与新值 |
| 约束 | 时间约束(temporal constraints) | 主键 / 唯一 / 外键可建立在范围类型上(外键需要 PERIOD) |
| 复制 | idle_replication_slot_timeout | 空闲超过阈值的复制槽自动失效,防 WAL 无限堆积 |
| 复制 | max_active_replication_origins | 复制原点数量上限与复制槽数量解耦 |
| 升级 | pg_upgrade 保留统计 | 升级后优化器统计就地保留,不再强制 vacuumdb --analyze-in-stages 全量重跑 |
| 可观测 | 统计视图增强 | pg_stat_io 字节计数、每后端 I/O/WAL 统计、EXPLAIN ANALYZE 默认带 BUFFERS 等 |
| 优化器 | 自连接消除、OR 转数组等 | 一批基于统计信息的查询改写优化 |
如何阅读一个大版本
PG 的每个大版本都会发布一份结构固定的 release notes:先是一段 Overview(主要特性摘要),然后是 Migration to Version N(不兼容变化,升级前必读),最后才是按模块分类的详细 Changes。阅读时有一个反直觉的原则:Overview 讲的是「多什么」,Migration 讲的是「变什么」,而升级事故几乎都出在后者。PG18 的 Migration 部分有一条对本教程读者影响很大的条目:initdb 默认启用数据校验和(checksums),而 pg_upgrade 要求新旧集簇的校验和设置一致——这直接影响容器的初始化脚本与升级流程,本页「注意事项」会展开。
本系列三页的分工
| 页面 | 主题 | 适合谁 |
|---|---|---|
| 本页(overview) | 核心新特性:I/O、表设计、索引、认证、监控 | 所有使用者 |
| cloud-native | 部署:Docker、Kubernetes、云托管、备份与可观测 | 运维与平台工程师 |
| ecosystem | 生态:扩展、发行版、驱动、社区与版本节奏 | 技术选型与长期规划者 |
语法与用法
下面用最小例子把最重要的几个特性「跑一遍」。每一段都可以直接在 PG18 的 psql 里执行或验证。
异步 I/O:三个新 GUC。 io_method 决定异步 I/O 的执行方式,只能在启动时设置:
# postgresql.conf:异步 I/O 三件套(新增于 PG18)
io_method = worker # worker(默认)| io_uring | sync
io_workers = 3 # 仅 io_method = worker 时生效,默认 3
io_combine_limit = 128kB # 合并 I/O 的上限,默认 128kB虚拟生成列:不再写 STORED。 PG18 起 GENERATED ALWAYS AS 默认就是虚拟列:
-- PG18:默认 VIRTUAL,读取时计算,不占存储
CREATE TABLE orders (
id bigint PRIMARY KEY,
price numeric(10,2) NOT NULL,
quantity integer NOT NULL,
amount numeric(12,2) GENERATED ALWAYS AS (price * quantity) VIRTUAL
);
-- 需要落盘(物化)时必须显式写 STORED,行为与 PG17 一致
CREATE TABLE orders_stored (
id bigint PRIMARY KEY,
amount numeric(12,2) GENERATED ALWAYS AS (id * 2) STORED
);UUIDv7:时间有序的主键。
SELECT uuidv7();
SELECT uuidv4(); -- PG18 新增的显式别名,等价于 gen_random_uuid()
SELECT uuid_extract_timestamp(uuidv7()); -- v7 也能提取时间戳了RETURNING OLD/NEW。 同一条 DML 里同时拿到改前与改后的值:
UPDATE accounts SET balance = balance - 100
WHERE id = 1
RETURNING OLD.balance AS before, NEW.balance AS after;EXPLAIN ANALYZE 默认带 BUFFERS。 不用再手动加 BUFFERS 选项:
EXPLAIN ANALYZE
SELECT count(*) FROM orders WHERE customer_id = 42;
-- 输出中每个节点自动附带 shared hit/read/dirtied/written 统计复制槽空闲超时。 一行配置防止失联订阅撑爆磁盘:
# 复制槽空闲(无复制连接使用)超过 1 小时后在检查点阶段失效
idle_replication_slot_timeout = 1h # 默认 0(禁用)详细说明
异步 I/O:io_method 与 io_workers
PG18 之前,一个后端进程读取数据页的方式是「同步阻塞」的:Seq Scan 想要第 N 页,就发起一次 pread() 然后等它返回;预读只能依赖操作系统的 readahead,或 effective_io_concurrency 引导的位图堆扫描预取(而那个参数在没有 posix_fadvise() 支持的系统上甚至只能保持 0)。PG18 引入的异步 I/O(AIO)子系统改变了这个模型:后端可以把多个读请求排队提交,在等待的同时做别的事,等一批请求齐了再统一处理。
对外接口是三个新 GUC 与一个新视图:
| 参数 / 视图 | 说明 |
|---|---|
io_method | 枚举:worker(用专用 I/O worker 进程执行异步 I/O,默认)、io_uring(用 Linux io_uring 接口,要求构建时带 --with-liburing)、sync(同步执行,即 PG17 及以前的行为)。只能启动时设置 |
io_workers | I/O worker 进程数量,默认 3;仅在 io_method = worker 时生效;只能在 postgresql.conf 或命令行设置 |
io_combine_limit | 合并 I/O 操作的最大尺寸,默认 128kB;受 io_max_combine_limit(默认 128kB,Unix 上通常最高可到 1MB)静默钳制 |
io_max_concurrency | 单个进程同时执行的 I/O 操作数上限,默认 -1 表示根据 shared_buffers 与最大进程数自动推算(不超过 64) |
pg_aios | 新系统视图,展示当前正在使用的异步 I/O 文件句柄 |
release notes 明确列出的受益场景是顺序扫描、位图堆扫描(bitmap heap scan)与 VACUUM 等批量读操作——这类操作天然是「一次发起多个不相关的页读请求」,异步化的收益最大。同时,effective_io_concurrency 与 maintenance_io_concurrency 的默认值被提高到 16,并且在没有 fadvise() 支持的系统上也允许设置大于 0 了——这一点对容器与云环境相当友好。
-- 观察 AIO 状态
SELECT * FROM pg_aios;需要注意 worker 与 io_uring 的选择逻辑:worker 模式零依赖、所有平台可用,是多出来的几个进程换跨平台兼容;io_uring 模式要求二进制构建时启用了 liburing,且运行在支持 io_uring 的 Linux 内核上,收益是更低的路径开销。如果拿不准,保持默认 worker 就是官方推荐姿势;sync 主要用于排查问题时的对照基线。
虚拟生成列:VIRTUAL 成为默认
生成列(generated column)是「值由表达式定义的列」。PG12 引入它时只有一种形态:GENERATED ALWAYS AS (expr) STORED——表达式在写入时求值并把结果物理存到表里,占空间、可索引、读取零成本。PG18 引入第二种形态 VIRTUAL:表达式在读取时求值,完全不占存储,但每次读都要计算,且不能直接建索引。
PG18 的语法变化有一个容易踩坑的点:GENERATED ALWAYS AS (expr) 不写关键字时,默认从 STORED 变成了 VIRTUAL。也就是说,PG17 之前写的 CREATE TABLE ... amount GENERATED ALWAYS AS (price * quantity) 这类语句,在 PG18 上语义变了——列不再物化。迁移既有 DDL 时必须逐条检查,凡依赖「读取零成本」或「可建索引」的生成列,务必显式写 STORED。
| 维度 | STORED(PG12+) | VIRTUAL(PG18+,默认) |
|---|---|---|
| 求值时机 | 写入时 | 读取时 |
| 占用存储 | 是 | 否 |
| 可否建索引 | 可以 | 不可以(需在其依赖列或表达式上建) |
| 更新成本 | 随写入增加 | 无 |
| 适用 | 读多写少、需要索引 | 计算便宜、写多读少的派生值 |
两者的共同约束仍然成立:表达式必须是不可变(immutable)的——不能引用其他生成列、不能使用子查询、不能调用非 immutable 函数。虚拟生成列的引入让「永远不该被物理存储的派生值」有了正确的表达方式,例如大宽表里一两个拼接展示用的字段,用 VIRTUAL 就不再白白放大表和 WAL。
UUIDv7:时间有序的 UUID
uuidv7() 生成 RFC 9562 定义的 version 7 UUID:48 位毫秒级 Unix 时间戳打头 + 亚毫秒精度 + 随机部分。因为时间戳在最前面,v7 的值随时间单调递增,作为主键插入 B-tree 索引时永远落在索引右侧——这与 gen_random_uuid()(v4)产生的「完全随机、写哪儿都是」形成鲜明对比。v4 UUID 做主键会导致 B-tree 频繁随机写、页分裂分散、缓存命中率低,这是过去十年里无数文章反复论证过的反模式;PG18 内置 uuidv7() 之后,官方直接给出了正解。
函数签名的完整行为(来自官方文档):
uuidv7 ( [ shift interval ] ) → uuid- 时间戳为「毫秒精度 UNIX 时间戳 + 亚毫秒部分 + 随机数」,时间可排序;
- 可选参数
shift把计算出的时间戳平移给定区间,可用于「生成落在过去/未来的 UUID」,不接受无穷区间值; - 平移后的时间戳必须落在 v7 的 48 位毫秒时间戳能表示的范围内(1970-01-01 00:00:00 UTC 到约公元 10889 年),越界报错;
- 同时新增
uuidv4()作为gen_random_uuid()的显式别名,让代码能表达「我要的就是 v4」; uuid_extract_timestamp()从 PG18 起对 v1 与 v7 都能提取时间戳。
实战建议:新表主键若选 UUID,默认用 uuidv7();对外暴露且在意「ID 泄露创建时间」的场景再权衡 v4。v7 的时间部分意味着从 ID 本身就能看出记录大致创建时间,这既是特性也是隐私考量,需要按业务判断。
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7(),
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 按 id 排序 ≈ 按时间排序,分页不再需要额外的时间排序列
SELECT * FROM events ORDER BY id DESC LIMIT 20;OAuth 2.0 认证
PG18 把 OAuth 2.0 加入了服务端认证方法家族。这一改动面向的是「客户端本来就有企业 IdP(身份提供方)」的组织:应用不再持有数据库密码,而是持有一个 OAuth 访问令牌(bearer token),PostgreSQL 服务端校验令牌后放行连接。
构成这套能力的四个部件(全部见于 release notes 与官方文档):
pg_hba.conf新增oauth认证方法:连接请求携带令牌,服务端走 OAuth 流程验证;oauth_validator_libraries服务端参数:配置用于校验令牌的验证器库(validator library),校验逻辑由外部库实现;- libpq 侧 OAuth 连接选项:客户端库提供了配套的 OAuth 选项组,驱动基于 libpq 即可获得该能力;
- 构建选项
--with-libcurl:编译时加入 libcurl 支持(OAuth 的发现文档获取依赖它)。
# pg_hba.conf(示意):对某网段启用 OAuth 认证
# host all all 10.0.0.0/8 oauth issuer=... scope=...参数细节(issuer、scope、验证器库的编写方式)属于官方 auth-oauth 文档的内容,本页不展开;可以确定的定位是:PG18 的 OAuth 是基础设施——它把「数据库认证」接入了现代零信任架构的版图,让短期令牌、集中撤销、按应用身份审计成为可能,但落地需要验证器库与 IdP 的配合,不是改一行 pg_hba.conf 就完事的传统方法。对多数团队,SCRAM-SHA-256 仍然是当下默认选择,OAuth 值得作为中期规划。
B-tree 跳过扫描(skip scan)
PG18 之前,多列 B-tree 索引 (a, b) 有个经典限制:查询条件必须约束到前导列 a,索引才有用。WHERE b = 42 只能走全表扫描(或另建 (b) 索引),因为 B-tree 的排序键是 (a, b) 拼起来的,没有 a 就无法定位。PG18 为 B-tree 实现了跳过扫描:当前导列没有约束或只有非等值约束、而后面的列有可用约束时,执行器把前导列的每个不同取值当作「隐式等值条件」逐段扫描——相当于把 WHERE b = 42 改写成 WHERE (a = :v1 AND b = 42) OR (a = :v2 AND b = 42) OR ... 但无需真的枚举。
release notes 对适用条件的表述值得原文照读:「当第一列或前面的列没有约束(或只有非等值约束)、而后面的列有可用的约束时,多列 btree 索引可以在更多场景下被使用」。从实现原理出发可以推出它的甜点区与雷区:前导列不同值(distinct)数量少时收益大(每段扫描都有边界可跳),前导列基数极高时,逐值跳跃的成本可能超过直接顺序扫描——优化器会基于统计信息做选择,但理解原理能帮你解释计划。
-- 索引:业务上主查询维度是 (tenant_id, created_at)
CREATE INDEX idx_orders_tenant_time ON orders (tenant_id, created_at);
-- PG18:即使没有 tenant_id 条件,只按时间段查询也可能用上该索引(跳过扫描)
EXPLAIN
SELECT * FROM orders
WHERE created_at BETWEEN '2026-09-01' AND '2026-09-30';跳过扫描的另一层意义是减少索引数量:过去「为了 B 列的查询再建一个 (B) 索引」在很多场景下可以省掉,直接降低写放大与存储成本。
GIN 并行构建与索引家族的其他改进
GIN 索引(全文搜索、JSONB、数组的主力索引)从 PG18 起支持并行构建:CREATE INDEX 可以动用并行 worker,与 B-tree 并行构建共享同一套机制。对大表上动辄以小时计的 GIN 建索引作业,这是实打实的等待时间削减。并行度仍由 max_parallel_maintenance_workers 控制,maintenance_work_mem 的口径也保持一致。
同一批索引相关改动还包括:
- 范围类型的 GiST 与 B-tree 索引构建提速:构建前先把值排序,减少构建过程中的随机 I/O 与比较成本;
- btree_gist 扩展的索引构建同样受益于值排序;
- 非 B-tree 的唯一索引可以用作分区键与物化视图(前提是索引类型支持等值判断)——此前这些位置强制要求 B-tree;
- amcheck 新增
gin_index_check(),GIN 索引终于有了官方一致性校验函数。
EXPLAIN 与查询分析增强
PG18 的 EXPLAIN 是历年来变化最密的一版,核心精神是「常用的东西默认给你」:
EXPLAIN ANALYZE自动包含 BUFFERS 输出——过去要手动写EXPLAIN (ANALYZE, BUFFERS),现在 BUFFERS 是默认项;- 每个索引扫描节点报告「索引查找次数」(index lookups)——终于可以区分「扫了多少索引页」与「回表了多少行」,对评估 skip scan、重复扫描类计划的成本极其有用;
- 行数支持小数(fractional row counts)——部分扫描节点的预估行数不再四舍五入;
- Materialize、Window Aggregate、CTE 节点输出内存与磁盘使用量——排查「排序溢出到磁盘」不再需要开
trace_sort; - 输出窗口函数的参数细节;
- Parallel Bitmap Heap Scan 报告 worker 缓存统计;
- 被禁用的节点(disabled node)在
EXPLAIN ANALYZE输出中标注——配合enable_*开关调试计划时一眼可见; EXPLAIN (WAL)输出完整的 WAL 缓冲写满计数。
生态侧还有两个配套改动:新扩展 pg_overexplain 提供更深层的调试信息(普通用户用不到、内核开发者离不开的那类),以及扩展可以注册自定义 EXPLAIN 选项(如 pg_stat_statements 生态可以按需插旗)。加上 PG17 已引入的 EXPLAIN (MEMORY) 等,EXPLAIN 家族正在从「计划文本打印器」进化为「性能观测框架」。
-- PG18:BUFFERS 默认开启,注意每个节点下的 Shared 统计
EXPLAIN ANALYZE
SELECT * FROM orders WHERE tenant_id = 7 ORDER BY created_at DESC LIMIT 50; QUERY PLAN
------------------------------------------------------------------------------------------------------
Limit (actual rows=50 loops=1)
Buffers: shared hit=214
-> Sort (actual rows=50 loops=1)
Sort Method: top-N heapsort Memory: 42kB
Buffers: shared hit=214
-> Index Scan using idx_orders_tenant_time on orders (actual rows=50 loops=1)
Index Cond: (tenant_id = 7)
Index Searches: 1
Buffers: shared hit=214
Planning Time: 0.191 ms
Execution Time: 0.542 ms对照 PG17 的输出,注意两处差异:每个节点下自动出现 Buffers: 行(无需手写 BUFFERS 选项),索引扫描节点下多出 Index Searches(PG18 新增的索引查找计数,skip scan 的「逐段跳转」在这里表现为大于 1 的值)。
复制与升级相关改进
idle_replication_slot_timeout。 复制槽最大的运维风险是「消费端失联 → WAL 无限保留 → 磁盘写满」。PG17 引入的 max_slot_wal_keep_size 从「保留量」上设限,PG18 补上了「空闲时长」维度:槽持续无活跃复制连接超过该时长后,在检查点阶段自动失效(invalidate)。默认 0 表示禁用;失效的判定依据槽的 pg_replication_slots.inactive_since;注意两点——失效发生在检查点所以有滞后,且备机上同步来的槽(synced 值为 true)与不保留 WAL 的槽不适用此机制。
-- 配合监控:槽空闲了多久
SELECT slot_name, active, inactive_since,
now() - inactive_since AS idle_for
FROM pg_replication_slots
WHERE slot_type = 'logical';max_active_replication_origins。 复制原点(replication origin)的数量上限过去寄生在 max_replication_slots 上,两者解耦后,需要大量原点而槽位不多的场景(级联、多源汇聚)不再被迫抬高槽配额。
pg_upgrade 的三连击,是 PG18 对「升级窗口」最直接的贡献:
- 保留优化器统计:升级完成后统计就地可用,不再出现「升级完第一天全表顺序扫描」的经典事故。注意扩展统计(extended statistics)不在保留范围;想禁用可用
--no-statistics; --jobs支持并行执行数据库检查;--swap选项:直接交换新旧数据目录(而非复制/克隆/硬链接),是几种模式里最快的,但代价是新集群初始不可回退。
配套工具同样升级:vacuumdb --missing-stats-only 只补缺失统计;pg_dump --statistics 与 --statistics-only 让统计可以独立于数据迁移;pg_createsubscriber 新增 --all(为所有库建逻辑副本)、--enable-two-phase 等;pg_recvlogical 新增 --enable-failover 对应 PG17 的故障转移槽。
统计视图与可观测性增强
PG18 的监控面改动量大、条目多,这里按「视图」归组(全部见于 release notes):
pg_stat_io:
- 新增字节计数列
read_bytes、write_bytes、extend_bytes,原有的op_bytes列(恒等于块大小)被移除——I/O 观测从「页数」升级为「字节数」,与块大小解耦; - 新增 WAL I/O 活动行(含 WAL receiver 的写入与对应 wait event),配合
track_wal_io_timing的语义调整(现在控制的是pg_stat_io中的 WAL 计时); - 代价是
pg_stat_wal视图删掉了wal_write、wal_sync、wal_write_time、wal_sync_time四列——依赖旧列的监控面板要迁移到pg_stat_io。
每后端统计:新函数 pg_stat_get_backend_io() 与 pg_stat_get_backend_wal() 返回单个后端的 I/O 与 WAL 统计,可用 pg_stat_reset_backend_stats() 单独清零。「哪个连接把 WAL 写爆了」这类问题有了直接的答案。
表级与维护:pg_stat_all_tables 及其变体新增 total_vacuum_time、total_autovacuum_time、total_analyze_time、total_autoanalyze_time 四列;VACUUM/ANALYZE 的代价延迟(cost delay)时间出现在日志、pg_stat_progress_vacuum / pg_stat_progress_analyze 与 VERBOSE 输出中(需 track_cost_delay_timing = on);ANALYZE VERBOSE 新增 WAL、CPU 与平均读统计;检查点视图 pg_stat_checkpointer 新增 num_done(真正完成的检查点数)与 slru_written;pg_stat_database 新增并行 worker 计数 parallel_workers_to_launch / parallel_workers_launched。
日志:log_connections 支持更细粒度(并新增连接各阶段耗时的选项);log_line_prefix 新增 %L 输出客户端 IP;新增 log_lock_failures 记录 SELECT ... NOWAIT 等锁获取失败。
优化器改进一览
PG18 优化器的改动以「改写(rewrite)」为主线,多数不需要任何配置即可受益:
| 改进 | 说明 | 开关 |
|---|---|---|
| 自连接消除 | 自动删除某些无意义的表自连接 | enable_self_join_elimination |
| OR 转数组 | OR 条件链转成 = ANY(array) 走更快的索引路径 | 无 |
IN (VALUES ...) 转 = ANY | 让优化器统计被更好地利用 | 无 |
SELECT DISTINCT 键重排 | 内部重排去重键以避免排序 | enable_distinct_reordering |
GROUP BY 函数依赖裁剪 | 唯一索引覆盖的列函数依赖其他列时,从分组键中剔除冗余列 | 无 |
HAVING 下推 | GROUPING SETS 上的部分 HAVING 下推到 WHERE 提前过滤 | 无 |
| Right Semi Join | 「存在至少一个匹配」类查询有了新的计划形态 | 无 |
| Merge join + incremental sort | 归并连接可使用增量排序 | enable_incremental_sort |
执行器侧另有两条重量级:哈希连接与 GROUP BY 的内存占用降低(EXCEPT 的哈希集合运算同样受益),以及访问大量分区时锁与计划阶段的性能改进。这些优化叠加 AIO 与 skip scan,构成了 PG18「什么都不改、升级即提速」的主体来源——当然,前提是统计信息跟得上,这也正是「pg_upgrade 保留统计」被列为招牌特性的原因。
升级后想确认某项改写是否生效,PG18 给了对应的开关做 A/B 对照:SET enable_self_join_elimination = off; 或 SET enable_distinct_reordering = off; 后重跑 EXPLAIN,对比计划差异即可定位收益来源。查询 ID 的计算逻辑也有两处调整:常量列表只取首尾常量参与 jumble、按关系名归并查询——这直接影响 pg_stat_statements 的归并行为,详见第三页生态篇。
RETURNING OLD/NEW 与约束增强
PG18 在 SQL 写入路径上有两个「日常可感」的增强。
RETURNING OLD/NEW。 过去 RETURNING 只能返回「新值」(INSERT / UPDATE)或「旧值」(DELETE),想同时拿到改前改后得先查一遍再改。PG18 允许在 INSERT / UPDATE / DELETE / MERGE 的 RETURNING 列表里使用 OLD 与 NEW 两个特殊别名,并且别名可以重命名以避免与表中同名列冲突(release notes 原文条目):
UPDATE accounts
SET balance = balance - 100
WHERE id = 1
RETURNING OLD.balance AS before, NEW.balance AS after; before | after
--------+--------
1500 | 1400
(1 row)审计日志、乐观锁校验、变更流水这类场景从此少一次往返查询。
时间约束(temporal constraints)。 对主键、唯一与外键约束,PG18 支持建立在范围之上——即「约束在一段时间维度上成立」。它面向的是业务系统里最常见的「有效期为约束」问题:同一张卡在任意时刻只能有一条生效记录,但历史记录可以并存:
-- 示意:有效期重叠视为冲突(具体语法以官方 CREATE TABLE 文档为准)
CREATE TABLE card_state (
card_id bigint,
state text NOT NULL,
valid_at tstzrange NOT NULL,
PRIMARY KEY (card_id, valid_at WITHOUT OVERLAPS)
);NOT NULL 约束体系化。 PG18 把列上的 NOT NULL 规范存进了 pg_constraint 系统目录:NOT NULL 约束可以命名、可以出现在外部表(foreign table)上、继承关系可控(ALTER TABLE ... ALTER CONSTRAINT ... [NO] INHERIT),还支持通过 ALTER TABLE 设置 NOT VALID 属性。「约束即目录记录」的模型至此完整。
另有两个分区表相关的增强(release notes 原文条目):分区表允许 NOT VALID 外键约束;分区表允许仅对父表删除约束(此前该操作被错误禁止)。
数据类型、函数与客户端工具拾遗
这批改动单个都不大,合起来让 PG18 的日用体验明显变好(全部出自 release notes):
| 新能力 | 示例 / 说明 |
|---|---|
| 数组排序 / 反转 | array_sort(arr)、array_reverse(arr),作用于第一维 |
| 字节串反转 | reverse(bytea) |
| 整数 ↔ bytea 互转 | 以补码语义存储 |
MIN() / MAX() | 支持数组与复合类型参数 |
| 大小写折叠 | casefold(),比 upper/lower 更全面的大小写无关比较(单字符可映射为多字符) |
| 周数提取 | EXTRACT(WEEK FROM ts) |
| 罗马数字 | to_number('XII', 'RN') |
| 校验值 | crc32() / crc32c() |
| 数学函数 | gamma() / lgamma() |
| JSON | jsonb 的 null 可转型为标量类型的 SQL NULL(此前报错);json{b}_strip_nulls() 可选移除数组中的 null |
| Unicode 16.0.0 | 更完整的字符集数据;全文搜索新增爱沙尼亚语词干 |
psql 侧,PG18 把「多路复用」能力交给了命令行:psql 现在可以解析、绑定与关闭命名预处理语句(\parse、\bind_named、\close_prepared),并新增一组管道(pipeline)元命令——\startpipeline / \syncpipeline / \sendpipeline / \endpipeline / \flushrequest / \flush / \getresults,把「一次往返发多条语句」的协议能力下放到交互工具。提示符与变量同步更新:%P 显示管道状态、PIPELINE_SYNC_COUNT 等 PIPELINE_* 变量、WATCH_INTERVAL 控制 \watch 默认间隔,列表命令可加 x 后缀进入扩展模式,\conninfo 改为表格化输出更多信息。
libpq 侧同样是「协议年」:线协议 3.2 上线(取消请求密钥提升为 256 位,需要服务端与客户端都支持 3.2 才生效);新增 PQfullProtocolVersion() 报告完整协议版本号;连接参数与环境变量可限定可接受的协议版本范围;search_path 变更会主动通知客户端(对连接池与 ORM 场景是重要修复);sslkeylogfile 连接参数支持导出 TLS 密钥材料用于调试。驱动生态对这些能力的适配节奏由各项目决定,方向已经明确——见第三页生态篇。
VACUUM 与 autovacuum 的改进
PG17 把 VACUUM 的内存账本重做了一遍,PG18 则在「冻结(freeze)策略」上继续演进:普通 VACUUM 允许主动冻结全可见(all-visible)页,而不是等到全表冻结周期才处理,摊薄了未来 anti-wraparound 全表冻结风暴的峰值,激进程度由新参数 vacuum_max_eager_freeze_failure_rate 控制(服务级与表级皆可)。另外两个小而实用:vacuum_truncate 把「VACUUM 末尾截断文件」做成服务级 GUC(此前只有表级存储参数),autovacuum_vacuum_max_threshold 允许用固定死元组数(而非纯百分比)作为触发 autovacuum 的兜底阈值。autovacuum_worker_slots 则让 autovacuum_max_workers 可以在运行时调高到预留上限而无需重启。
实战示例
示例一:验证异步 I/O 生效
# 确认 io_method(默认 worker)
psql -c "SHOW io_method;"
psql -c "SHOW io_workers;" io_method
-----------
worker
(1 row)-- 跑一个顺序扫描密集的查询,同时观察 pg_aios
SELECT count(*) FROM big_table; -- 另一会话执行SELECT io_worker, io_task, relation, count(*)
FROM pg_aios
GROUP BY 1, 2, 3;pg_aios 里出现排队的读取任务即说明 AIO 通道在工作;若 io_method = sync,该视图基本为空。
示例二:虚拟生成列与 STORED 的对比
CREATE TABLE demo_v (id int, a int, b int,
s int GENERATED ALWAYS AS (a * b) STORED,
v int GENERATED ALWAYS AS (a * b));
INSERT INTO demo_v (id, a, b) SELECT g, g, g * 2 FROM generate_series(1, 100000) g;
-- 占用对比:VIRTUAL 列不占表存储
SELECT pg_total_relation_size('demo_v') AS bytes; bytes
---------
5275648
(1 row)同样的两列如果都是 STORED,体积会更大(多一个物理列)。反过来,若给 VIRTUAL 列当过滤条件高频查询,就要考虑改回 STORED 或直接给表达式建索引。
示例三:uuidv7 主键与时间提取
CREATE TABLE t_uuid7 (
id uuid PRIMARY KEY DEFAULT uuidv7(),
note text
);
INSERT INTO t_uuid7 (note) SELECT 'row ' || g FROM generate_series(1, 5) g;
SELECT id, uuid_extract_timestamp(id) AS ts, uuid_extract_version(id) AS ver
FROM t_uuid7
ORDER BY id; id | ts | ver
-------------------------------------+-----------------------------+-----
01993f1a-...-...-...-... | 2026-10-07 15:40:01.123+08 | 7
01993f1a-... | 2026-10-07 15:40:01.124+08 | 7
...注意 id 的字典序与 ts 的时间序一致——这正是 v7 的核心价值。
示例四:升级后验证统计被保留
-- pg_upgrade 前在旧集群记录
SELECT tablename, attname, n_distinct FROM pg_stats
WHERE tablename = 'orders' AND attname = 'customer_id';用 pg_upgrade 升级到 PG18 后重新执行同一查询:统计行仍然在——这是 PG18 之前做不到的(旧版本升级后 pg_stats 为空,必须等 autovacuum 或手动 ANALYZE)。升级脚本层面,pg_upgrade --jobs 4 --swap(视场景)与升级后的 vacuumdb --missing-stats-only --all 组合可以把窗口压到最小。
示例五:给复制槽上「空闲保险丝」
# postgresql.conf(发布端)
idle_replication_slot_timeout = 2h-- 验证机制存在与当前槽状态
SELECT slot_name, active, inactive_since, safe_wal_size
FROM pg_replication_slots;断开测试订阅端 2 小时后(并等一次检查点),该槽在 pg_replication_slots 中状态转为失效——max_slot_wal_keep_size 管「留多少」,idle_replication_slot_timeout 管「空多久」,两者配合构成完整的槽生命周期护栏。
示例六:PG18 监控新列速查
-- 表级维护耗时(PG18 新增四列)
SELECT relname,
total_vacuum_time,
total_autovacuum_time,
total_analyze_time,
total_autoanalyze_time
FROM pg_stat_user_tables
ORDER BY total_autovacuum_time DESC NULLS LAST
LIMIT 5;
-- 检查点视图的新列
SELECT num_timed, num_requested, num_done, slru_written
FROM pg_stat_checkpointer;
-- 库级并行 worker 计数(PG18 新增)
SELECT datname, parallel_workers_to_launch, parallel_workers_launched
FROM pg_stat_database;这些列补齐了「VACUUM 到底花了多久」「检查点真的完成了几个」「并行度有没有被卡」三类过去只能靠日志拼凑的问题。
示例七:观察 B-tree 跳过扫描
-- 造数据:前导列基数低,第二列基数高
CREATE TABLE demo (id bigint, low_cardinality text, value bigint);
INSERT INTO demo
SELECT g, (g % 8)::text, g % 100
FROM generate_series(1, 1000000) g;
CREATE INDEX idx_demo ON demo (low_cardinality, value);
ANALYZE demo;-- 只按第二列过滤——PG17 及以前必然全表扫描
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM demo WHERE value = 99; QUERY PLAN
---------------------------------------------------------------
Aggregate (actual rows=1 loops=1)
-> Index Only Scan using idx_demo on demo (actual rows=12500 loops=1)
Index Searches: 8
Heap Fetches: 0
Planning Time: 0.203 ms
Execution Time: 3.412 ms要点有二:Index Searches: 8 表示执行器在前导列的 8 个不同取值上分别「下探」了一次——这正是跳过扫描的形态;PG17 及以前的版本同样的查询只会得到 Seq Scan。是否选择跳过扫描最终由优化器基于统计与成本决定,用这条对照实验即可确认你手上的负载能否受益。
注意事项
initdb 默认启用数据校验和。 PG18 起 initdb 默认开启 checksums(可用 --no-data-checksums 关闭),而 pg_upgrade 要求新旧集簇校验和设置一致。升级流程脚本需要相应调整:老集簇若未开校验和,要么在新集簇 initdb 时加 --no-data-checksums 保持一致,要么借这次升级顺势开启(更推荐,但要在演练中验证性能影响)。
生成列默认语义变了。 GENERATED ALWAYS AS (expr) 在 PG18 默认为 VIRTUAL。所有既有 DDL 里没写 STORED 的生成列定义,升到 PG18 后行为都会改变(不再物化、不可索引)。升级评审时这是必查项。
MD5 密码认证进入弃用通道。 CREATE ROLE / ALTER ROLE 设置 MD5 密码时会输出弃用告警(可用 md5_password_warnings = off 关闭),官方明确未来大版本将移除 MD5。仍在用 MD5 的集群应把迁移 SCRAM-SHA-256 排上日程。
COPY FROM 读 CSV 时 \. 不再是 EOF 标记。 服务端 COPY FROM 处理 CSV 时,\. 必须单独成行且不再被当作文件结束符(psql 从 STDIN 读时仍按 EOF 处理,旧版 psql 连 PG18 服务端可能遇到 \copy 兼容问题)。有以 \. 分隔多段 CSV 的老脚本要改。
VACUUM / ANALYZE 默认处理继承子表。 现在会对父表的继承子表一并处理,旧行为用 ONLY 选项恢复。依赖「对父表 VACUUM 不碰子表」这个旧语义的维护脚本需要检查。
pg_stat_wal 丢失四列。 wal_write / wal_sync / wal_write_time / wal_sync_time 已移除,WAL I/O 观测迁移到 pg_stat_io(字节计数 + WAL 行)。所有 Grafana 面板、巡检脚本里引用旧列的要改。
AFTER 触发器的执行角色变化。 AFTER 触发器现在以「触发事件入队时」的活动角色执行,而非「触发器执行时」的角色。在事务中途 SET ROLE 再提交的场景下,触发器内逻辑的权限语义会不同。
AIO 不是免费午餐。 io_method = worker 会常驻 io_workers(默认 3)个进程;io_uring 需要构建期与内核双重支持。I/O 型负载是否能兑现 release notes 描述的收益,取决于存储介质与访问模式——升级后用真实负载压测,别只看跑分。
全文搜索/ pg_trgm 升级后建议重建索引。 PG18 让全文搜索改用集簇默认 collation provider(如 ICU、builtin)读取配置与词典,非 libc 的集簇升级后行为可能变化,官方建议升级后重建相关索引。
时区缩写解析顺序变化。 PG18 起优先使用当前会话的时区缩写定义,其次才检查 timezone_abbreviations 参数(此前顺序相反)。依赖该参数全局定制缩写语义的部署要留意。
无日志分区表被禁止。 CREATE UNLOGGED 的分区表不再允许创建(此前行为不完整),对分区表执行 ALTER TABLE SET [UN]LOGGED 也不再是无操作;相关脚本要调整。
GRANT / REVOKE 的 rule 权限清理。 自 PG8.2 起就无效的 rule 权限被正式移除,权限巡检脚本中对 rule 的授权语句会报错,应顺手清理。
最佳实践
- 升级前通读官方 release notes 的 Migration 章节,而不是只看 Overview。 PG18 的校验和默认值、生成列默认语义、
COPYCSV、触发器角色四处不兼容都藏在 Migration 里,任何一条都足以让升级日变成事故日。 - 新表主键默认用
uuidv7(),而不是gen_random_uuid()。 时间有序带来的 B-tree 友好性是白拿的性能;确实需要不可预测性(防枚举)的对外标识再选 v4,并注意 v7 会暴露创建时间。 - 写生成列 DDL 时显式声明
VIRTUAL或STORED。 依赖默认值的代码在 PG17 与 PG18 之间语义不同;显式关键字既是自文档,也是跨版本可移植性的保险。 - 给所有复制槽设置
idle_replication_slot_timeout,并与max_slot_wal_keep_size组合。 前者管「空多久失效」,后者管「最多保留多少 WAL」,双保险才能彻底封死「失联订阅撑爆磁盘」这条路。 - 升级流程加入
--jobs与统计保留验证。pg_upgrade --jobs N并行检查、升级后核对pg_stats有数据、必要时vacuumdb --missing-stats-only补漏——把「升级后第一天变慢」从玄学变成流程。 - EXPLAIN ANALYZE 先看 BUFFERS 与 index lookups。 PG18 默认输出让「缓存命中率」与「索引实际下探次数」成为读计划的第一动作,尤其是评估 skip scan 是否按预期工作时。
- 多列索引设计重新审视「前导列等值」教条。 在前导列基数有限的前提下,
(低基数列, 时间列)这类索引现在能服务只按时间过滤的查询,可以省掉部分单列索引;用EXPLAIN验证选择器是否真的选中了 skip scan。 - 把
pg_stat_io纳入监控面板,迁移掉pg_stat_wal旧列。 字节计数比页计数更贴近容量规划;WAL I/O 行配合track_wal_io_timing能回答「WAL 写入的时间花在哪」。 - autovacuum 阈值用「百分比 + 固定上限」双轨。
autovacuum_vacuum_max_threshold给超大表一个绝对值兜底,避免「1% of 10 亿行 = 1000 万死元组才触发」的滞后。 - OAuth 作为认证架构的中期规划。 有企业 IdP 的团队可以先在测试环境验证
oauth方法与验证器库的集成路径,把 SCRAM → OAuth 的迁移写进安全路线图;没条件的团队先把 MD5 清干净。 - 用
sync模式做 A/B 对照。 怀疑 AIO 引入的收益时,切io_method = sync重跑关键负载,量化差异后再决定是否进一步调io_workers/io_combine_limit——用数据说话。 - 保持对官方文档的引用习惯。 本页所有参数名、默认值、行为描述都以 PostgreSQL 18 release notes 为准;跨版本写脚本时,先查对应版本文档再下笔。
下一节
继续学习:PG18:云原生与部署