Skip to content

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.0pg_hba.conf 新增 oauth 认证方法,配合 oauth_validator_libraries
索引B-tree 跳过扫描多列 B-tree 索引在前导列无等值条件时也能被使用
索引GIN 并行构建GIN 索引创建可以走并行 worker
SQLRETURNING OLD/NEWINSERT / 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 的执行方式,只能在启动时设置:

conf
# 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 默认就是虚拟列:

sql
-- 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:时间有序的主键。

sql
SELECT uuidv7();
SELECT uuidv4();   -- PG18 新增的显式别名,等价于 gen_random_uuid()
SELECT uuid_extract_timestamp(uuidv7());  -- v7 也能提取时间戳了

RETURNING OLD/NEW。 同一条 DML 里同时拿到改前与改后的值:

sql
UPDATE accounts SET balance = balance - 100
WHERE id = 1
RETURNING OLD.balance AS before, NEW.balance AS after;

EXPLAIN ANALYZE 默认带 BUFFERS。 不用再手动加 BUFFERS 选项:

sql
EXPLAIN ANALYZE
SELECT count(*) FROM orders WHERE customer_id = 42;
-- 输出中每个节点自动附带 shared hit/read/dirtied/written 统计

复制槽空闲超时。 一行配置防止失联订阅撑爆磁盘:

conf
# 复制槽空闲(无复制连接使用)超过 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_workersI/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 了——这一点对容器与云环境相当友好。

sql
-- 观察 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() 之后,官方直接给出了正解。

函数签名的完整行为(来自官方文档):

sql
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 本身就能看出记录大致创建时间,这既是特性也是隐私考量,需要按业务判断。

sql
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 与官方文档):

  1. pg_hba.conf 新增 oauth 认证方法:连接请求携带令牌,服务端走 OAuth 流程验证;
  2. oauth_validator_libraries 服务端参数:配置用于校验令牌的验证器库(validator library),校验逻辑由外部库实现;
  3. libpq 侧 OAuth 连接选项:客户端库提供了配套的 OAuth 选项组,驱动基于 libpq 即可获得该能力;
  4. 构建选项 --with-libcurl:编译时加入 libcurl 支持(OAuth 的发现文档获取依赖它)。
conf
# 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)数量少时收益大(每段扫描都有边界可跳),前导列基数极高时,逐值跳跃的成本可能超过直接顺序扫描——优化器会基于统计信息做选择,但理解原理能帮你解释计划。

sql
-- 索引:业务上主查询维度是 (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 家族正在从「计划文本打印器」进化为「性能观测框架」。

sql
-- PG18:BUFFERS 默认开启,注意每个节点下的 Shared 统计
EXPLAIN ANALYZE
SELECT * FROM orders WHERE tenant_id = 7 ORDER BY created_at DESC LIMIT 50;
text
                                              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 的槽不适用此机制。

sql
-- 配合监控:槽空闲了多久
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 原文条目):

sql
UPDATE accounts
SET balance = balance - 100
WHERE id = 1
RETURNING OLD.balance AS before, NEW.balance AS after;
text
 before | after
--------+--------
   1500 |   1400
(1 row)

审计日志、乐观锁校验、变更流水这类场景从此少一次往返查询。

时间约束(temporal constraints)。 对主键、唯一与外键约束,PG18 支持建立在范围之上——即「约束在一段时间维度上成立」。它面向的是业务系统里最常见的「有效期为约束」问题:同一张卡在任意时刻只能有一条生效记录,但历史记录可以并存:

sql
-- 示意:有效期重叠视为冲突(具体语法以官方 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()
JSONjsonb 的 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 生效 ​

bash
# 确认 io_method(默认 worker)
psql -c "SHOW io_method;"
psql -c "SHOW io_workers;"
text
 io_method
-----------
 worker
(1 row)
sql
-- 跑一个顺序扫描密集的查询,同时观察 pg_aios
SELECT count(*) FROM big_table;   -- 另一会话执行
sql
SELECT io_worker, io_task, relation, count(*)
FROM pg_aios
GROUP BY 1, 2, 3;

pg_aios 里出现排队的读取任务即说明 AIO 通道在工作;若 io_method = sync,该视图基本为空。

示例二:虚拟生成列与 STORED 的对比 ​

sql
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;
text
  bytes
---------
 5275648
(1 row)

同样的两列如果都是 STORED,体积会更大(多一个物理列)。反过来,若给 VIRTUAL 列当过滤条件高频查询,就要考虑改回 STORED 或直接给表达式建索引。

示例三:uuidv7 主键与时间提取 ​

sql
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;
text
                 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 的核心价值。

示例四:升级后验证统计被保留 ​

sql
-- 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 组合可以把窗口压到最小。

示例五:给复制槽上「空闲保险丝」 ​

conf
# postgresql.conf(发布端)
idle_replication_slot_timeout = 2h
sql
-- 验证机制存在与当前槽状态
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 监控新列速查 ​

sql
-- 表级维护耗时(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 跳过扫描 ​

sql
-- 造数据:前导列基数低,第二列基数高
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;
sql
-- 只按第二列过滤——PG17 及以前必然全表扫描
EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM demo WHERE value = 99;
text
                          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 的授权语句会报错,应顺手清理。

最佳实践 ​

  1. 升级前通读官方 release notes 的 Migration 章节,而不是只看 Overview。 PG18 的校验和默认值、生成列默认语义、COPY CSV、触发器角色四处不兼容都藏在 Migration 里,任何一条都足以让升级日变成事故日。
  2. 新表主键默认用 uuidv7(),而不是 gen_random_uuid()。 时间有序带来的 B-tree 友好性是白拿的性能;确实需要不可预测性(防枚举)的对外标识再选 v4,并注意 v7 会暴露创建时间。
  3. 写生成列 DDL 时显式声明 VIRTUAL 或 STORED。 依赖默认值的代码在 PG17 与 PG18 之间语义不同;显式关键字既是自文档,也是跨版本可移植性的保险。
  4. 给所有复制槽设置 idle_replication_slot_timeout,并与 max_slot_wal_keep_size 组合。 前者管「空多久失效」,后者管「最多保留多少 WAL」,双保险才能彻底封死「失联订阅撑爆磁盘」这条路。
  5. 升级流程加入 --jobs 与统计保留验证。 pg_upgrade --jobs N 并行检查、升级后核对 pg_stats 有数据、必要时 vacuumdb --missing-stats-only 补漏——把「升级后第一天变慢」从玄学变成流程。
  6. EXPLAIN ANALYZE 先看 BUFFERS 与 index lookups。 PG18 默认输出让「缓存命中率」与「索引实际下探次数」成为读计划的第一动作,尤其是评估 skip scan 是否按预期工作时。
  7. 多列索引设计重新审视「前导列等值」教条。 在前导列基数有限的前提下,(低基数列, 时间列) 这类索引现在能服务只按时间过滤的查询,可以省掉部分单列索引;用 EXPLAIN 验证选择器是否真的选中了 skip scan。
  8. 把 pg_stat_io 纳入监控面板,迁移掉 pg_stat_wal 旧列。 字节计数比页计数更贴近容量规划;WAL I/O 行配合 track_wal_io_timing 能回答「WAL 写入的时间花在哪」。
  9. autovacuum 阈值用「百分比 + 固定上限」双轨。 autovacuum_vacuum_max_threshold 给超大表一个绝对值兜底,避免「1% of 10 亿行 = 1000 万死元组才触发」的滞后。
  10. OAuth 作为认证架构的中期规划。 有企业 IdP 的团队可以先在测试环境验证 oauth 方法与验证器库的集成路径,把 SCRAM → OAuth 的迁移写进安全路线图;没条件的团队先把 MD5 清干净。
  11. 用 sync 模式做 A/B 对照。 怀疑 AIO 引入的收益时,切 io_method = sync 重跑关键负载,量化差异后再决定是否进一步调 io_workers / io_combine_limit——用数据说话。
  12. 保持对官方文档的引用习惯。 本页所有参数名、默认值、行为描述都以 PostgreSQL 18 release notes 为准;跨版本写脚本时,先查对应版本文档再下笔。

下一节

继续学习:PG18:云原生与部署

参考链接 ​