聚焦六大功能:PostgreSQL 18 新特性深度解析
PostgreSQL 全球开发组于 2025 年 5 月 8 日发布了 PostgreSQL 18 的首个 Beta 版本,正式版也已于 9 月 25 日正式上线。本文 IvorySQL 社区将为大家拆解 PostgreSQL 18 的六大亮点特性。
一、PG 异步 I/O(AIO)框架:迈出打破同步阻塞瓶颈的第一步
PostgreSQL 18 全新引入异步 I/O 子系统。新机制允许特定场景下并行执行多个异步预读操作,CPU 无需等待数据返回即可继续推进查询,一定程度上降低了等待损耗。此框架为 PG 未来更深入更彻底的异步 I/O 性能优化奠定基础,迈出了第一步。
核心提升场景
【当前仅实现异步读,没 有实现异步写】 所有 Seq Scan 场景下通过适配过异步 I/O 的 ReadStream 设施可实现并行化顺序预读,提升 Seq Scan 的性能,效果好于原有 posix_fadvice 的建议性预读。尤其在云存储场景下单次阻塞读 I/O 相比本地 I/O 所需时间更长,异步 I/O 加持下的并行化预读的优势更加明显。目前异步 I/O 已支持顺序扫描、位图堆扫描和 VACUUM 操作的异步读取,早期测试显示,读取密集型查询性能可提升 2-3 倍。
如图所示:

使用了异步 I/O 的 ReadStream 机制可以在收到读请求后异步地预读后续可能使用的 buffer。而在使用同步 I/O 方式在每次请求读取 buffer 时,都要等待 I/O 操作完成,这样降低了系统吞吐量。
使用方法
# - I/O -
#backend_flush_after = 0 # measured in pages, 0 disables
effective_io_concurrency = 300 # 1-1000; 0 disables issuing multiple simultaneous IO requests
maintenance_io_concurrency = 300 # 1-1000; same as effective_io_concurrency
io_max_combine_limit = 256kB # usually 1-128 blocks (depends on OS)
# (change requires restart)
io_combine_limit = 256kB # usually 1-128 blocks (depends on OS)
io_method = io_uring # worker, io_uring, sync
# (change requires restart)
io_max_concurrency = 128 # Max number of IOs that one process
# can execute simultaneously
# -1 sets based on shared_buffers
# (change requires restart)
#io_workers = 3 # 1-32;
用户可选择三种不同的 io_method 启用异步 I/O,分别是:
worker若干后台 I/O workers 接收处理后端进程的 I/O 请求。io_uringLinux 系统中 io_uring 子系统通过操作系统内核线程处理 PG 的 I/O 请求。sync满足异步 I/O 框架接口要求的同步 I/O。
要启用异步 I/O,用户需要根据自身情况设定上述 GUC 参数。其中每个进程能够拥有的最大异步 I/O 句柄为 io_max_concurrency,用户可以将其置 -1,使数据库自行选择合适的值。若自行选择的值太大,则可能因为异步 I/O 所占空间太大而无法启动数据库;若自行选择的值太小则无法完全发挥出异步 I/O 性能。
启动数据库后,用户可通过 pg_aios 视图实时地获取当前系统异步 I/O 执行状况:
postgres=# select * from pg_aios;
-[ RECORD 1 ]---+-------------------------------------------
pid | 85834
io_id | 14208
io_generation | 204
state | SUBMITTED
operation | readv
off | 116252672
length | 8192
target | smgr
handle_data_len | 1
raw_result |
result | UNKNOWN
target_desc | block 14191 in file "base/5/16427"
f_sync | f
f_localmem | f
f_buffered | t
...
各列含义详见官方文档。
框架设计
PG 18 引入异步 I/O 框架,支持通过 GUC 参数灵活配置异步 I/O:包括实现方式(io_method,可选 worker、io_uring 或 sync)、并发规模(如 *_io_concurrency、io_max_concurrency)及实现相关参数(如 io_workers)。
该框架对 I/O 目标(当前支持 smgr,未来计划支持 WAL)和不同阶段、不同数据源(shared buffer/local buffer)的行为进行了抽象(通过 PgAioHandleCallbacks 结 构),以支持后续扩展。相关内存于启动时在共享内存中分配,后续不再缩放。进程按编号访问所属异步 I/O 资源,句柄通过 generation 号标记复用。
目前该版本异步 I/O 主要提供对 smgr 的异步读支持,尚不支持 WAL 异步读写,smgr 的异步写入功能仍在开发中。
对原有设施的修改
- 扩展 smgr 接口:新增
smgr_startreadv方法以支持异步读取。 - 实现回调结构:smgr 需实现
PgAioTargetInfo和PgAioHandleCallBacks回调结构。 - 适配现有模块:smgr 和 buffer manager 等模块需填充异步 I/O 抽象结构以兼容框架。
- PG 临界区处理:同步 I/O 可在 PG 临界区内发起;异步 I/O 因分段执行且带回调,需移除回调中可能失败的操作(临界区内一切操作不能失败,如用
RelPathStr替代 palloc 的char*字符串)以确保安全。 - 优化上层接口:利用异步 I/O 改造 ReadStream 等接口,实现真预读,大幅提升顺序扫描、pg_prewarm 及 ANALYZE 等操作的 I/O 性能,效果优于原有 posix_fadvise 方案。
使用注意及未来展望
- **io_uring 需要较新内核:**旧版 Linux Kernel 不支持 io_uring。某些早期版本内核虽然支持 io_uring,但功能和性能表现与新内核有一定差距。
- 架构限制并发粒度:受多进程架构所限,PG 异步 I/O 期间可并行运行的计算任务较少,难以实现更细粒度的任务级异步。当前主要性能收益集中于ReadStream 顺序预读及等并行 I/O 操作。
- 未来可能的性能提升:Linux io_uring 支持直接 I/O(DIO)特性,为在 PG 中启用 DIO 奠定基础。未来启用直接 I/O 可免除双重 buffer(OS 层面对 I/O 数据进行 buffer,PG 层面对 I/O 进行 buffer)以减少不必要的数据复制。在高速 NVMe 上还配合 DIO 启用IORING_SETUP_IOPOLL选项使用轮训方式检查 I/O 完成情况,还可以进一步提升性能。
- 未来更多的异步 I/O 后端:除了
sync模式,正式版的 PostgreSQL 18 仅支持worker和io_uring两种异步 I/O 后端。目前异步 I/O 框架设计已基本完备,未来版本有望支持 Windows IORing,IOCP,以及 Posix 异步 I/O 等 I/O 后端,为用户提供更多选择。
小结
PostgreSQL 18 异步 I/O 框架提升数据库系统 I/O 能力,同时也增强了 PostgreSQL 架构的可扩展性,用户只需要根据自身情况修改 GUC 参数即可获取到异步 I/O 带来的好处。目前异步 I/O 框架设计基本完备,后期支持其他异步 I/O 后端也将非常方便。任意平台 Postgres 用户可以尝试 io_method = worker或者sync 若要在非官方适配 io_uring 的旧 Linux 内核发行版上使用 io_uring 后端,需要进行充分测试后再使用。
二、 跳跃式扫描:让 B 树索引 “提速换挡”
在 PostgreSQL 18 之前的版本中,多列 B 树索引可用于包含该索引中任意子集列的查询条件,在对起始(最左侧)列施加约束时最为高效。对前导列的等式约束,加上对第一个不带有等式约束的列的任何不等式约束,用于限制要扫描的索引部分。
例如,给定一个基于(a,b,c)非空字段的升序索引和查询条件 WHERE a = 5 AND b >= 42 AND c < 77,该索引将从具有 a = 5 和 b = 42 的第一个条目开始扫描,一直扫描到最后具有 a = 5 的条目。 具有 c >= 77 的索引条目将被跳过,但它们仍需扫描。
原则上,这种索引可以用于对 b 和/或 c 有约束条件而对 a 没有约束条件的查询——但必须扫描整个索引,所以在大多数情况下优化器更倾向于对表进行顺序扫描表,而非利用索引扫描。
核心提升场景
从 PostgreSQL 18 开始:
如果 B 树索引扫描能够应用跳跃式扫描(SKIP SCAN),在遍历索引时应用每个列的约束,可以减少索引的读取。跳跃式扫描的工作原理是内部生成一个动态等式约束,该约束与索引列中的每个可能值相匹配。
效果测试
对比版本:
PostgreSQL 17 vs PostgreSQL 18
表结构与索引
CREATE TABLE t1
(
c1 int
c2 int,
c3 float
)
WITH (fillfactor=80);
CREATE INDEX idx_t1_c1c2 ON t1(c1, c2);
数据生成
INSERT INTO t1
SELECT (random()*1000)::int, (random()*10000)::int, random()
FROM generate_series(1,1000000) g;
通过 COPY 导入数据
COPY t1 FROM '/.../t1.csv' WITH (FORMAT csv);
查询语句 使用复合索引的第二个列
EXPLAIN ANALYZE SELECT * FROM t1 WHERE c2=100;
PostgreSQL 17 执行计划 选择使用并行顺序扫描
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..12986.33 rows=100 width=16) (actual time=1.125..76.076 rows=90 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on t1 (cost=0.00..11976.33 rows=42 width=16) (actual time=1.414..68.624 rows=30 loops=3)
Filter: (c2 = 100)
Rows Removed by Filter: 333303
Planning Time: 0.792 ms
Execution Time: 76.165 ms
(8 rows)
关闭顺序扫描强制选择索引扫描,并非最优计划,执行更慢。
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_t1_c1c2 on t1 (cost=0.42..18773.42 rows=100 width=16) (actual time=1.846..100.758 rows=90 loops=1)
Index Cond: (c2 = 100)
Planning Time: 0.147 ms
Execution Time: 100.806 ms
(4 rows)
PostgreSQL 18 执行计划选择使用索引扫描,可以看出跳跃式扫描执行效率提升幅度非常大
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Index Scan using idx_t1_c1c2 on t1 (cost=0.42..3900.84 rows=100 width=16) (actual time=0.225..11.464 rows=90.00 loops=1)
Index Cond: (c2 = 100)
Index Searches: 1002
Buffers: shared hit=3096
Planning Time: 0.141 ms
Execution Time: 11.522 ms
(6 rows)
关闭索引扫描和位图扫描强制选择顺序扫描。
postgres=# EXPLAIN ANALYZE SELECT * FROM t1 WHERE c2=100;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..12986.33 rows=100 width=16) (actual time=1.486..86.881 rows=90.00 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=6768
-> Parallel Seq Scan on t1 (cost=0.00..11976.33 rows=42 width=16) (actual time=2.758..78.712 rows=30.00 loops=3)
Filter: (c2 = 100)
Rows Removed by Filter: 333303
Buffers: shared hit=6768
Planning Time: 0.141 ms
Execution Time: 86.926 ms
(10 rows)
使用注意
跳跃式扫描目前只能支持等值比较条件。
小结
PostgreSQL 18 的索引跳跃式扫描,使得多列 BTREE 索引能够被那些仅对第二个或之后的索引列进行等值引用的查询使用,大幅减少索引扫描需要访问的条目,使其效率得到明显提升。
三、 虚拟生成列:存储与计算的 “灵活平衡”
PostgreSQL 18 开发体验相关的特性,聚焦于简化开发流程、提升代码灵活性,让开发者更高效地利用 PostgreSQL 能力。
IvorySQL 数据库长期致力于 Oracle 特性兼容,其中包含了一项虚拟列的语法兼容:
column [datatype][generated always] AS (column_expression)[VIRTUAL]
这次 PostgreSQL 18 终于也带来了虚拟列功能。虚拟列是一种不存储数据的表列,其值在查询时通过动态计算得出。与存储列相比,虚拟列节省了列存储空间,查询虚拟列值时通过计算虚拟列表达式的值作为该列的值。
基本语法
PostgreSQL 18 中虚拟列的语法和存储列的语法相似,新增加关键字VIRTUAL,当省略 STORED 和 VIRTUAL 关键字时默认为虚拟列。其语法如下所示:
GENERATED ALWAYS AS ( generation_expr ) [ STORED | VIRTUAL ]
虚拟列用例
虚拟列的标识是在列的限制条件中表示的,通过虚拟列的限制语法标识列为虚拟列,以下为虚拟列表的创建、查询和新增虚拟列:
-- 创建包含虚拟列的表,其中price_with_tax为虚拟列
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL,
tax_rate NUMERIC(5,2) DEFAULT 0.20,
price_with_tax NUMERIC(10,2) GENERATED ALWAYS AS (price * (1 + tax_rate)) VIRTUAL
);
-- 插入数据
INSERT INTO products (name, price, tax_rate)
VALUES ('Laptop', 1000.00, 0.20);
-- 查询数据(虚拟列自动计算)
SELECT name, price, tax_rate, price_with_tax FROM products;
name | price | tax_rate | price_with_tax
--------+---------+----------+----------------
Laptop | 1000.00 | 0.20 | 1200.00
(1 row)
--为表添加虚拟列
ALTER TABLE products ADD COLUMN selling_price NUMERIC(10,2)
GENERATED ALWAYS AS (
price * (1 - 0.2) * (1 + tax_rate)
) virtual;
实现原理浅析
虚拟列的创建
创建的表中虚拟列的存储方式和普通列的存储方式类似,其列信息都存储在 pg_attribute 系统表中,其中 attgenerated 列存 储生成列信息,如果该列的值为's',表示该列为存储列。PostgreSQL 18 新增的虚拟列在该字段中的标识符为'v',并且将虚拟列的表达式存储于 pg_attrdef 系统表中。
--查看虚拟列信息,其attgenerated为v表示该列为虚拟列
postgres=# select * from pg_attribute where attname='price_with_tax';
-[ RECORD 1 ]--+---------------
attrelid | 16388
attname | price_with_tax
atttypid | 1700
attlen | -1
attnum | 5
atttypmod | 655366
attndims | 0
attbyval | f
attalign | i
attstorage | m
attcompression |
attnotnull | f
atthasdef | t
atthasmissing | f
attidentity |
attgenerated | v
attisdropped | f
attislocal | t
attinhcount | 0
attcollation | 0
attstattarget |
attacl |
attoptions |
attfdwoptions |
attmissingval |
--查看虚拟列表达式存储,可以看到以下表达式为虚拟列表达式
postgres=# select pg_get_expr(adbin, adrelid) from pg_attrdef where adnum = 5;
pg_get_expr
-------------------------------------
(price * ((1)::numeric + tax_rate))
(1 row)
虚拟列的插入或更新
由于虚拟列的数据不占据存储空间,所以任何指定更新或插入虚拟列的操 作都将被限制。
--指定插入虚拟列
INSERT INTO products (name, price, tax_rate, price_with_tax) VALUES ('Laptop', 1000.00, 0.20, 1);
ERROR: cannot insert a non-DEFAULT value into column "price_with_tax"
DETAIL: Column "price_with_tax" is a generated column.
--指定更新虚拟列
update products set price_with_tax = 1 where name = 'Laptop';
ERROR: column "price_with_tax" can only be updated to DEFAULT
DETAIL: Column "price_with_tax" is a generated column.
虚拟列的查询
PostgreSQL 18 中查询虚拟列的实现是在生成执行计划 阶段完成。在逻辑重写优化阶段,判断查询的范围表中是否包含虚拟列,如果包含虚拟列,则将该虚拟列的表达式从 pg_attrdef 中获取出来并替换原虚拟列名。这样查询虚拟列的值就相当于计算其表达式的值,即 select price_with_tax 相当于 select (price \* ('1'::numeric + tax_rate)) as price_with_tax。可以看到以下虚拟列被替换成了其表达式:
postgres=# explain verbose SELECT name, price, tax_rate, price_with_tax
FROM products;
QUERY PLAN
----------------------------------------------------------------------
Seq Scan on public.products (cost=0.00..23.12 rows=750 width=76)
Output: name, price, tax_rate, (price * ('1'::numeric + tax_rate))
(2 rows)
适用场景
- 当考虑存储空间时,可以使用虚拟列,因为虚拟列不占用磁盘空间。
- 当列的值需要根据依赖的列变化而变化时,需要使用虚拟列。因为虚拟列的值是动态获取的。
- 当虚拟列表达式简单时,可以使用虚拟列。因为查询虚拟列需消耗 CPU 资源,表达式复杂会消耗太多 CPU 资源。
- 因为 Oracle 中有虚拟列功能,更加方便 Oracle 的虚拟列迁移至 PostgreSQL 中。
待完善部分
一些功能目前尚不支持,但可能会作为增量功能在后续的版本中添加:
- 在虚拟列上创建索引或使用虚拟列。
- 虚拟列上也没有唯一约束。
- 虚拟列上的扩展统计信息。
- 虚拟列上的外键约束。
- 虚拟列上的非空约束(支持检查约束)。
- ALTER TABLE / DROP EXPRESSION。
- 虚拟列不能具有域类型。
- 逻辑复制不支持虚拟列。
小结
虚拟列与普通列和存储列有着本质的不同,因为虚拟列的值不占磁盘空间,其获取值的方式也与普通列和存储列不同,普通列或存储列需要从磁盘获取数据,而虚拟列是通过动态计算获取虚拟列的值。
四、UUID 功能增强:有序性与易用性提升
传统 UUID 的无序性是其用作主键的主要痛点:
传统 UUID(尤其是 v4)的完全随机性是其作为数据库主键的痛点:
- UUID 随机生成,插入位置不确定,导致索引树频繁分裂和重组,大幅降低写入性能。
- 破坏聚簇索引(如 InnoDB)的物理存储顺序,增加磁盘 I/O。
- 范围查询和排序效率低下,性能低下。
UUIDv7 的关键突破:时间有序性架构设计
UUIDv7 通过在 UUID 的高位部分引入时间戳来解决生成 UUID 完全随机的问题,使新生成的 UUID 能够按照创建时间自然排序。这样,B 树索引可以像自增整数一样进行顺序插入,同时仍然保持 UUID 的全局唯一性和分布式生成优势。
该特性使 UUIDv7 作为主键具备以下突出优势:
- 严格按照创建时间先后顺序递增。
- 减少索引碎片。
- 提高缓存命中率。
- 适合高并发插入和高效查询的场景。
UUIDv7 的结构设计

| 字段 | 位数 | 说明 |
|---|---|---|
| 毫秒级 Unix 时间戳 | 48 位 | Unix 时间戳(毫秒) |
| 亚毫秒级时间戳分数(用于额外排序) | 12 位 | 时间戳的微秒精度扩展 |
| 随机数 | 62 位 | 随机数或计数器 |
| 版本号 | 4 位 | 固定为 0111(v7) |
| 变体 | 2 位 | 固定为 10(RFC 4122) |
设计关键点解析:
- 高精度时间前缀(48 位): 精确到毫秒的 Unix 时间戳,确保 ID 严格按时间递增(需 NTP 时钟同步)。
- 尾部随机位(62 位): 保证分布式唯一性,避免 v1 版本的 MAC 地址泄漏风险。
有序性如何解决性能问题?
- B-Tree 索引优化: 新生成的 UUIDv7 总是大于之前的值,因此被追加到索引尾部,避免中间节点分裂。
- 缓冲池 友好: 顺序写入使新记录集中在少数数据页。当页写满时,数据库只需分配新页追加,减少旧页淘汰与磁盘 I/O。
- 范围查询加速: 时间有序性使 WHERE id > '2025-06-01' 可转化为时间戳范围过滤,大幅降低扫描范围。
如何在 PostgreSQL18 中应用 UUIDv7
PostgreSQL 18 引入了多个新函数来支持 UUIDv7,方便生成、操作和提取 UUID 信息。
uuidv7()函数:用于生成新的 UUIDv7 值
-- 使用当前时间戳生成 UUIDv7
SELECT uuidv7();
-- 输出示例: 0197f96c-b278-7f64-a32f-dae3cabe1ff0
-- 生成 1 小时前的 UUIDv7
SELECT uuidv7(INTERVAL '-1 hour');
-- 生成 30 分钟后的 UUIDv7
SELECT uuidv7(INTERVAL '30 minutes');
uuidv4()函数:作为已有函数gen_random_uuid()的别名 ,便于和 uuidv7 一起使用
-- 两者等价
SELECT gen_random_uuid();
SELECT uuidv4();
uuid_extract_timestamp()函数 : 该函数现在支持 UUIDv7(原本只支持 UUIDv1)
-- 从 UUIDv7 提取时间戳
SELECT uuid_extract_timestamp(uuidv7());
-- 示例输出: 2025-09-18 12:20:49.409+00
uuid_extract_version()函数:用于检测 UUID 的版本:
-- 检查 UUID 版本
SELECT uuid_extract_version(uuidv7()); -- 返回 7
SELECT uuid_extract_version(uuidv4()); -- 返回 4
PostgreSQL 数据库中使用 UUIDv7 作为主键:
--创建带 UUIDv7 主键的表
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuidv7(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
这样,每条新记录的 id 都会自动分配一个按时间戳排序的 UUID。
--插入数据
INSERT INTO users (username, email)
VALUES ('alice', 'alice@example.com');
INSERT INTO users (username, email)
VALUES ('bob', 'bob@example.com');
-- 按 UUID 时间顺序查看
SELECT id, username, uuid_extract_timestamp(id) as uuid_timestamp
FROM users
ORDER BY id;
性能优势:
- UUIDv7 的时间戳顺序能显著减少页分裂和缓存失效,有效提升 B 树索引效率。
--创建性能测试表
CREATE TABLE performance_test (
id_v4 UUID DEFAULT uuidv4(),
id_v7 UUID DEFAULT uuidv7(),
data TEXT DEFAULT 'sample data'
);
--使用UUIDv7作为索引
CREATE INDEX idx_v4 ON performance_test (id_v4);
CREATE INDEX idx_v7 ON performance_test (id_v7);
批量插入后,你可以用 pg_statio_user_indexes 查看索引命中情况,UUIDv7 通常表现更优。
- UUIDv7 自带时间排序,大部分场景下显著提升排序性能。
-- 利用 UUIDv7 自然排序
SELECT id_v7, data
FROM performance_test
ORDER BY id_v7
LIMIT 10;
相比 UUIDv4 的随机顺序,UUIDv7 查询结果按创建顺序返回,更直观。
UUIDv7 最佳实践

适合使用 UUIDv7 的场景:
-
多租户应用:可用 UUIDv7 做主键,并为
(tenant_id, id)创建复合索引,既保持唯一性又能按时间排序。 -
分布式系统:多个服务可独立生成 UUIDv7 ,并且在全局范围内仍能保持时间顺序。
限制与注意事项
- 依赖系统时钟:需启用 NTP 等时间同步机制,避免时钟漂移。
- 时间戳精度:UUIDv7 以毫秒为单位,在同一毫秒内生成多个 UUID,顺序可能无法完全反映真实创建顺序,但仍保持唯一性。
- 迁移规划:从 UUIDv4 迁移到 UUIDv7 时,需要检查应用逻辑、索引和外部依赖。
小结
PostgreSQL 18 对 UUIDv7 的支持,解决了 UUID 作为主键的性能瓶颈。UUIDv7 在保持全局唯一性的同时,具备类似自增整数的顺序性,使 B 树插入更高效,查询更快。
对于需要分布式、高并发和高性能的现代应用,UUIDv7 提供了一种兼顾唯一性和性能的实用解决方案。
五、EXPLAIN 增强:直观呈现执行细节
PostgreSQL 18 对 EXPLAIN 命令进行了重大升级,通过提供更丰富、更直观的执行计划信息,让数据库开发者和 DBA 能够更加轻松地进行查询性能分析与优化。
