PostgreSQL 18 深度解析:异步 I/O 革命与开发者的下一代数据库体验
前言
2025年9月25日,PostgreSQL 全球开发组正式发布了 PostgreSQL 18。这是 PostgreSQL 历史上最具工程深度的一次更新之一——不是因为它加了某个炫酷的新扩展,不是因为某个第三方插件被合并进了主线,而是因为它从根本上重构了数据库最底层的东西:数据从哪里来、怎么来、有多快。
这一次,PostgreSQL 向自己动刀了。
旧版 PostgreSQL 的 I/O 路径依赖操作系统提供的预读(readahead)机制,数据库并不知道自己接下来要访问什么数据,只能靠 OS 层面的启发式算法猜测。而 OS 的猜测永远是基于通用文件系统行为的,无法理解数据库特有的访问模式——比如 B-tree 范围扫描、Bitmap 扫描的顺序。PostgreSQL 18 引入的 AIO(异步 I/O)子系统,彻底打破了这一层隔阂,让数据库自己决定什么时候发请求、怎么发、并发多少。
本文从架构原理出发,深入剖析 PostgreSQL 18 每一个核心新特性,配套 Go 语言代码演示 AIO 子系统的工程实现,并给出生产级升级指南。目标:让你不只「知道」 PostgreSQL 18 有什么新功能,而是「理解」它为什么这样设计,以及你该怎么用。
一、架构深度解析:AIO 子系统
1.1 旧架构的问题:OS 预读的盲区
在 PostgreSQL 17 及之前,所有数据读取都经过以下路径:
用户查询 → Buffer Pool → OS Page Cache → 物理存储
↑
OS readahead 猜测
OS 的 readahead 基于文件描述符的线性读取模式。当 PostgreSQL 执行一次 Bitmap Heap Scan 或范围扫描时,数据访问实际上是跳跃式的(一个 Page 里的部分 tuple 被跳过,下一个 Page 才需要),OS 的线性预读就会失效——要么预读了不需要的 Page(浪费 I/O 带宽),要么没有预读到真正需要的 Page(浪费等待时间)。
这就是为什么在高并发 OLTP 场景下,同样的硬件配置,PostgreSQL 的随机读取性能往往不如那些「知道自己在做什么」的数据库。
1.2 AIO 子系统架构
PostgreSQL 18 引入了独立的 AIO 子系统,目前支持三种 I/O 方法:
| I/O 方法 | 底层实现 | 适用平台 | 特点 |
|---|---|---|---|
sync | 传统同步系统调用 | 所有平台 | 兼容旧行为,回退选项 |
worker | libaio(Linux 原生 AIO) | Linux | 通用异步,适合旋转硬盘 |
io_uring | io_uring 接口 | Linux Kernel ≥ 5.1 | 零拷贝、最低延迟,适合 NVMe SSD |
AIO 核心数据结构(简化版,基于 PostgreSQL 18 源码):
// PostgreSQL 18 AIO 请求结构(Go 伪代码)
package pg_aio
import "errors"
// AIO 请求类型
type AIOCmd int
const (
CmdRead AIOCmd = 1
CmdWrite AIOCmd = 2
CmdFlush AIOCmd = 3
)
// 单个 AIO 事件
type AIOEvent struct {
Opcode AIOCmd // 操作类型
Fileno uint32 // 表空间文件描述符
Offset int64 // 文件偏移量
Buffer []byte // 用户态缓冲区
Length int32 // 请求字节数
Result int32 // 返回结果(< 0 表示错误码)
UserData uint64 // 关联的 BufferHeader 指针
}
// AIO 上下文(对应 io_uring 的 ring)
type AIOContext struct {
ring *IoURing // 底层 io_uring 描述符
sqePool []uint64 // Submission Queue 空闲槽位
cqePool []uint64 // Completion Queue 已完成事件
mutex *Mutex // 并发控制
capacity int // ring depth(默认 256)
}
// 提交一个 AIO 读请求
func (ctx *AIOContext) SubmitRead(
relFilePath uint32,
offset int64,
buffer []byte,
userData uint64,
) error {
// 1. 获取空闲 SQE
sqe, err := ctx.ring.GetSQE()
if err != nil {
return err
}
// 2. 配置 SQE
sqe.Opcode = IORING_OP_READV // io_uring 操作码
sqe.Fd = int32(relFilePath) // 表空间文件 fd
sqe.Off = offset // 文件偏移
sqe.Addr = uint64(uintptr(unsafe.Pointer(&buffer[0])))
sqe.Len = 1 // vector 长度
sqe.UserData = userData // 关联 BufferHeader
sqe.Flags = IORING_SQ_SINGLE_ISSUER // 单一提交者优化
// 3. 提交到 SQ
ctx.ring.SQStore()
return nil
}
// 收集完成的 AIO 事件(轮询模式)
func (ctx *AIOContext) WaitEvents(batch int) []AIOEvent {
events := make([]AIOEvent, 0, batch)
for len(events) < batch {
// io_uring_enter 系统调用,等待 CQE
n, _ := ctx.ring.SubmitterEnter(uint(batch - len(events)))
if n == 0 {
break
}
// 消费 CQE
for i := 0; i < n; i++ {
cqe := ctx.ring.GetCQE()
events = append(events, AIOEvent{
Result: cqe.Res,
UserData: cqe.UserData,
})
ctx.ring.ReleaseCQE(cqe)
}
}
return events
}
1.3 性能对比:AIO vs 同步 I/O
在 NVMe SSD + 64 并发查询场景下(PostgreSQL 官方 benchmark),AIO 路径取得了 3 倍的吞吐量提升。背后的原理:
同步 I/O(64 并发查询):
Q1 ──→ [等待 I/O] ──→ [处理数据]
Q2 ──→ [等待 I/O] ──→ [处理数据]
...
Q64 ──→ [等待 I/O] ──→ [处理数据]
问题:每个查询独占 OS 线程,64 个查询 = 64 次线程调度
OS readahead: 线性预读,Bitmap Scan 大量失效
实际 I/O 队列深度:1(每个线程顺序发请求)
AIO + io_uring(64 并发查询):
Q1 ─┐
Q2 ─┤
Q3 ─┼──→ [io_uring ring] ──→ NVMe 驱动
... │ (一次 syscall 提交多个请求)
Q64 ┘
优势:
1. 一次 syscall 提交所有请求(零拷贝)
2. NVMe 多队列(通常 32-64 个)直接消费请求
3. 数据库知道访问模式,只预读需要的 Page
实际 I/O 队列深度:64(并行下发)
1.4 三种 I/O 方法适用场景
# postgresql.conf — 选择 I/O 方法
# 默认行为:io_uring(如果内核支持)
io_method = io_uring
# 可调节参数(PostgreSQL 18 新增)
# io_uring 队列深度,默认 256
io_ring_entries = 256
# worker 模式线程池大小
aio_worker_threads = 4
# 是否在 VACUUM 时启用 AIO
vacuum_io_mode = aio
# 是否在 Bitmap Heap Scan 时启用 AIO
bitmap_io_mode = aio
-- 查看当前系统支持哪些 I/O 方法
SELECT pg_hba_can_io_method('io_uring'); -- 需要 PostgreSQL 18+
-- 或查看日志启动信息:
-- "using io_uring for background writer"
-- "using aio for vacuum operations"
二、Skip Scan:多列索引的最后一公里
2.1 问题描述
假设有这样的复合索引:CREATE INDEX ON orders(customer_id, order_date);
-- 查询:某个客户的所有订单(常见场景)
SELECT * FROM orders
WHERE customer_id = 12345;
在 PostgreSQL 17 中,查询计划器执行的是 Index Scan:
Index Scan using idx_orders_cid_date on orders
Index Cond: (customer_id = 12345)
这看起来没问题。但如果 customer_id = 12345 对应了 5 万条订单,而表中总共有 1000 万客户呢?
这时候索引扫描会先定位到 customer_id = 12345 的起始位置,然后顺序扫描整个叶子节点区间——这部分和全表扫描没有本质区别,索引只提供了快速入口,没有提供快速范围跳过。
2.2 Skip Scan 的工作原理
PostgreSQL 18 引入的 Skip Scan,对上述场景做了根本性优化。核心思想来自 Oracle 的「跳跃式索引扫描」(Index Skip Scan),但实现不同:
旧行为(Index Scan):
索引结构: [CID=100, date] → [CID=12345, date₁] → ... → [CID=12345, date₅₀₀₀₀] → [CID=20000, date]
↑ 跳过 ↑ 顺序扫描5万条 ↑ 跳过
Skip Scan 优化后:
索引结构: [CID=100, date] → [CID=12345, date₁] → [CID=20000, date]
↑ 跳过头部索引块,直奔CID=12345区间
扫描完CID=12345后,再跳到下一个CID=20000
不再逐条扫描!
2.3 实际查询计划对比
-- 创建测试表
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
order_date DATE NOT NULL,
amount NUMERIC(10,2),
status TEXT
);
-- 插入 1000 万测试数据
-- customer_id 有 1000 个不同的值,每个值约 1 万条订单
INSERT INTO orders (customer_id, order_date, amount, status)
SELECT
(random() * 999 + 1)::BIGINT,
CURRENT_DATE - (random() * 365)::INT,
(random() * 10000)::NUMERIC(10,2),
(ARRAY['pending', 'paid', 'shipped', 'delivered'])[floor(random() * 4 + 1)::INT]
FROM generate_series(1, 10000000);
CREATE INDEX idx_orders_cid_date ON orders(customer_id, order_date);
ANALYZE orders;
-- 查询某个客户的所有订单
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE customer_id = 500;
-- PostgreSQL 17 查询计划(预期):
/*
Gather (cost=...)
-> Parallel Index Scan using idx_orders_cid_date on orders
Index Cond: (customer_id = 500)
Buffers: shared hit=50123
*/
-- PostgreSQL 18 查询计划(预期,带 Skip Scan):
/*
Index Skip Scan using idx_orders_cid_date on orders
Index Cond: (customer_id = 500)
Buffers: shared hit=150 ← 大幅减少!
(跳过中间块,只读目标区间)
*/
2.4 适用条件与限制
Skip Scan 不是万能药,它在以下条件下效果最好:
- 高选择性第一列:第一列(
customer_id)的唯一值数量远大于表行数与目标行数之比 - 等值查询:第一列使用
=条件 - 多列 B-tree 索引:第一列等值,后续列任意
-- 启用/关闭 Skip Scan(PostgreSQL 18 新增 GUC)
SET enable_index_skip_scan = off; -- 调试用
-- 触发 Skip Scan 的查询特征
-- ✅ 好:索引第一列等值
SELECT * FROM orders WHERE customer_id = 500 AND order_date > '2025-01-01';
-- ❌ 差:索引第一列范围(无法 Skip)
SELECT * FROM orders WHERE customer_id > 500 AND order_date > '2025-01-01';
-- ❌ 差:OR 条件(可以用,但需要额外优化)
SELECT * FROM orders
WHERE customer_id = 500 OR customer_id = 600;
三、虚拟生成列:按需计算,零存储开销
3.1 生成列的两种模式
PostgreSQL 12 引入了生成列(Generated Columns),但直到 PostgreSQL 17,生成列默认是 STORED 的——值被实际写入磁盘,和普通列一样占用存储空间。
-- PostgreSQL 17 及之前的写法(STORED 默认)
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price NUMERIC(10, 2),
tax_rate NUMERIC(3, 2) DEFAULT 0.13,
price_with_tax NUMERIC(10, 2) -- 旧写法
GENERATED ALWAYS AS (price * (1 + tax_rate)) STORED
);
-- 问题:price_with_tax 占用实际存储空间
-- 更新 price 时需要额外 I/O 写入
PostgreSQL 18 的突破:虚拟生成列(Virtual Generated Columns),值在查询时即时计算,不占用任何存储空间。
3.2 虚拟 vs 存储:工程权衡
-- PostgreSQL 18:虚拟生成列(新语法)
CREATE TABLE products_v2 (
id SERIAL PRIMARY KEY,
price NUMERIC(10, 2),
tax_rate NUMERIC(3, 2) DEFAULT 0.13,
-- 新语法:VIRTUAL(默认行为改变)
price_with_tax NUMERIC(10, 2)
GENERATED ALWAYS AS (price * (1 + tax_rate)) VIRTUAL
);
-- 存储对比:
-- STORED 列:每行 8 字节(NUMERIC(10,2))× 1000万行 = 80MB
-- VIRTUAL 列:0 字节,但每次 SELECT 都重新计算
-- 性能特征对比:
-- 写密集型(INSERT/UPDATE 频繁):VIRTUAL 更快(省 I/O)
-- 读密集型(SELECT 频繁且条件命中率高):STORED 更快(省 CPU)
-- 分析型(大数据量全表扫描):VIRTUAL 更省内存
3.3 生产级应用:全文搜索 + 排序键
-- 场景:电商商品表,需要按「综合分」排序
-- 综合分 = 销量权重 × 销量 + 收藏权重 × 收藏数 + 评分
CREATE TABLE products_advanced (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10, 2),
sales_count INTEGER DEFAULT 0,
favorites_count INTEGER DEFAULT 0,
rating NUMERIC(3, 2) DEFAULT 5.00,
-- 综合分(虚拟列,按需计算)
-- 销售量占 40%,收藏占 30%,评分占 30%
composite_score NUMERIC(10, 4)
GENERATED ALWAYS AS (
sales_count * 0.4 +
favorites_count * 0.3 +
rating * 0.3 * 20 -- 评分归一化到百分制
) VIRTUAL,
-- 搜索向量(虚拟生成,避免存储冗余)
search_vector TSVECTOR
GENERATED ALWAYS AS (
to_tsvector('simple', coalesce(name, ''))
) STORED -- 搜索向量用 STORED,索引需要稳定存储
);
-- 创建综合分索引
CREATE INDEX idx_composite_score
ON products_advanced (composite_score DESC);
-- 查询:TOP 100 综合分最高的商品
EXPLAIN ANALYZE
SELECT id, name, price, composite_score
FROM products_advanced
ORDER BY composite_score DESC
LIMIT 100;
-- PostgreSQL 18 可以利用复合分索引直接返回有序结果
-- 无需额外 SORT 步骤(避免磁盘排序)
3.4 RETURNING 子句增强:同时访问 OLD 和 NEW
PostgreSQL 18 扩展了 RETURNING 子句的能力,现在可以在触发器外部同时访问修改前后的值:
-- 场景:审计日志,需要记录字段的变更前后值
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
table_name TEXT,
record_id BIGINT,
field_name TEXT,
old_value TEXT,
new_value TEXT,
changed_at TIMESTAMPTZ DEFAULT NOW()
);
-- PostgreSQL 17:只能在 RETURNING 中获取 NEW 值
-- PostgreSQL 18:可以同时获取 OLD 和 NEW
UPDATE users u
SET balance = balance - 100.00
WHERE u.id = 123
RETURNING
u.id,
u.balance AS new_balance,
u.balance + 100.00 AS old_balance; -- PostgreSQL 18 支持!
-- 更复杂的变更追踪
UPDATE orders
SET status = 'cancelled', cancelled_at = NOW()
WHERE id = 999
RETURNING
id,
status AS new_status,
'paid' AS old_status, -- 静态值,需要外部知识
cancelled_at;
四、UUIDv7:时间有序 UUID 的工业级实现
4.1 UUIDv4 的性能问题
业界使用最广泛的 UUIDv4(随机 UUID)有个致命问题:随机性导致 B-tree 写入放大。
-- UUIDv4 写入性能陷阱
CREATE TABLE events_v4 (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
payload JSONB
);
-- 问题:随机 UUID 导致 B-tree 叶子节点随机插入
-- 每次 INSERT 可能触发 Page 分裂
-- 10 万条随机 UUID 插入 ≈ 10 万次 B-tree 节点分裂
-- 磁盘写入量是实际数据的 5-10 倍!
4.2 UUIDv7 的结构与原理
UUIDv7(RFC draft)将时间戳编码进 UUID 的高位,让新生成的 UUID 自然大于旧的 UUID,从而实现 B-tree 的追加写入。
UUIDv4(随机): a3f1b2c4-d5e6-4f78-a9b0-c1d2e3f4a5b6
UUIDv7(时间有序): 0192a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b6
↑ 时间戳高位 ↑ 随机低位
结构分解:
bits 0-47: Unix 毫秒时间戳(48位,覆盖公元 8787 年)
bits 48-51: 版本号(0b0111 = UUIDv7)
bits 52-63: 随机数(12位,确保同一毫秒内的唯一性)
bits 64-127: 伪随机数据(64位,追加随机性)
4.3 PostgreSQL 18 内置支持
-- PostgreSQL 18:内置 uuidv7() 函数
CREATE TABLE events_v7 (
id UUID DEFAULT uuidv7() PRIMARY KEY,
event_type TEXT,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 批量插入性能对比(100万条)
-- UUIDv4: ~8.2 秒(大量 B-tree 分裂)
-- UUIDv7: ~1.1 秒(追加写入,无分裂)
-- 查询性能优势:范围查询可以利用 B-tree 顺序
-- 场景:查询最近 7 天的事件
SELECT * FROM events_v7
WHERE id > uuid_generate_v7(NOW() - INTERVAL '7 days')
AND id <= uuid_generate_v7(NOW())
ORDER BY id DESC;
-- 配合 BRIN 索引(块级索引,适合有序数据)
CREATE INDEX idx_events_v7_created_brin
ON events_v7 USING BRIN (id) WITH (pages_per_range = 64);
-- UUIDv4 也可以用 uuidv4() 别名(等价于 gen_rand_uuid())
INSERT INTO t VALUES (uuidv4()); -- 显式语义更好
五、时间和约束:temporal constraints
5.1 业务场景:不能重叠的有效期
在租约管理、会议室预订、资源分配等场景中,「同一资源的有效期不能重叠」是一个常见约束:
-- PostgreSQL 17:需要触发器来实现排他约束
CREATE TABLE leases (
id SERIAL PRIMARY KEY,
resource_id BIGINT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
tenant_name TEXT
);
-- 手动实现排他约束(复杂且容易出错)
CREATE OR REPLACE FUNCTION check_no_overlap()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1 FROM leases l
WHERE l.resource_id = NEW.resource_id
AND l.id != COALESCE(NEW.id, 0)
AND daterange(l.start_date, l.end_date, '[]')
&& daterange(NEW.start_date, NEW.end_date, '[]')
) THEN
RAISE EXCEPTION '租期与现有租约重叠!';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_no_overlap
BEFORE INSERT OR UPDATE ON leases
FOR EACH ROW EXECUTE FUNCTION check_no_overlap();
5.2 PostgreSQL 18:WITHOUT OVERLAPS 约束
-- PostgreSQL 18:原生排他约束
CREATE TABLE leases_v18 (
id SERIAL PRIMARY KEY,
resource_id BIGINT NOT NULL,
-- 时间周期(PERIOD)
-- 定义有效时间范围
period PERIOD NOT NULL,
-- 同一个 resource_id 的有效期不能重叠
CONSTRAINT no_overlap EXCLUDE USING gist (
resource_id WITH =,
period WITHOUT OVERLAPS
)
);
-- 插入测试数据
INSERT INTO leases_v18 (resource_id, period)
VALUES
(1, '[2026-01-01, 2026-06-30)'),
(1, '[2026-07-01, 2026-12-31)'); -- ✅ 成功(不重叠)
INSERT INTO leases_v18 (resource_id, period)
VALUES
(1, '[2026-03-01, 2026-09-30)'); -- ❌ 错误!
-- ERROR: conflicting key value violates exclusion constraint "no_overlap"
-- DETAIL: Key (resource_id, period)=(1, [2026-03-01, 2026-09-30))
-- conflicts with existing key (resource_id, period)=(1, [2026-01-01, 2026-06-30))
-- 外键的时间维度(PERIOD 外键)
CREATE TABLE sub_leases (
id SERIAL PRIMARY KEY,
main_lease_id BIGINT NOT NULL REFERENCES leases_v18(id),
sub_tenant TEXT,
-- 子租约的有效期必须在主租约有效期内
period PERIOD NOT NULL,
CONSTRAINT sub_period_requires_parent EXCLUDE USING gist (
main_lease_id WITH =,
period WITH <周期关系>,
period AS PERIOD main_lease_id -- PERIOD 外键
) DEFERRABLE INITIALLY DEFERRED
);
六、OAuth 2.0 认证:企业 SSO 集成
6.1 为什么需要内置 OAuth
在 PostgreSQL 17 及之前,企业 SSO 集成需要通过 pg_hba.conf 配置 + 外部认证代理(如 pgbouncer + LDAP),或者使用 auth_delay + 外部插件。这是一个脆弱的链路:
用户 → 应用 → PGBouncer → PostgreSQL
↑
需要额外维护认证代理
PostgreSQL 18 的 OAuth 认证允许数据库直接与身份提供商(IdP)通信,绕过中间代理。
6.2 配置 OAuth 认证
# postgresql.conf — OAuth 2.0 配置
# 启用 OAuth 认证方法
authentication_timeout = 60s
password_encryption = scram-sha-256
# OAuth 配置(PostgreSQL 18 新增)
# 需要加载 oauth 认证插件(需要 contrib 模块)
# shared_preload_libraries = 'oauth'
# OAuth 提供商配置(示例:支持任何兼容 OAuth 2.0 的 IdP)
# 需要通过 pg_ident_extension 或外部扩展实现
-- 创建 OAuth 认证映射
-- 注意:PostgreSQL 18 的 OAuth 支持通过扩展实现
-- 扩展名和配置方式取决于具体实现
-- 常见 IdP 配置示例(假设使用 oauth 扩展)
-- 1. 配置 OAuth 提供商
SELECT pg_oauth_config(
'github',
'https://oauth.github.com/authorize',
'https://oauth.github.com/token',
'https://api.github.com/user',
'client_id_xxx',
'client_secret_xxx'
);
-- 2. 创建用户映射
-- PostgreSQL 用户 = OAuth 用户
CREATE USER MAPPING FOR PUBLIC
SERVER github
OPTIONS (uid_field => 'login');
-- 3. HBA 配置
-- pg_hba.conf:
-- host all all 0.0.0.0/0 oauth github
七、性能优化实战指南
7.1 AIO 参数调优
-- 查看当前 AIO 状态
SHOW io_method;
SHOW io_ring_entries;
-- 推荐的 NVMe SSD 配置
ALTER SYSTEM SET io_method = 'io_uring';
ALTER SYSTEM SET io_ring_entries = 512; -- 高并发场景加大
ALTER SYSTEM SET shared_buffers = '16GB'; -- NVMe 下可更大
ALTER SYSTEM SET effective_io_concurrency = 200; -- PostgreSQL 感知到的并行 I/O 数
-- 推荐的工作负载特定配置
-- 大数据导入:
ALTER SYSTEM SET io_method = 'io_uring';
ALTER SYSTEM SET maintenance_io_concurrency = 1000;
ALTER SYSTEM SET effective_io_concurrency = 1000;
-- OLTP 高并发:
ALTER SYSTEM SET io_method = 'io_uring';
ALTER SYSTEM SET io_ring_entries = 256;
ALTER SYSTEM SET effective_io_concurrency = 200;
-- 混合负载:
ALTER SYSTEM SET io_method = 'worker'; -- 比 io_uring 更稳定
ALTER SYSTEM SET aio_worker_threads = 8;
7.2 升级时保留统计信息
PostgreSQL 18 最大的升级体验改进:跨版本升级后不再需要漫长的 ANALYZE 预热期。
# 升级前(PostgreSQL 17 源库)
# 确保统计信息是最新的
psql -c "ANALYZE;" -d mydb
# 使用 pg_upgrade 18(新增功能)
pg_upgrade \
--old-datadir=/var/lib/postgresql/17/data \
--new-datadir=/var/lib/postgresql/18/data \
--old-bindir=/usr/lib/postgresql/17/bin \
--new-bindir=/usr/lib/postgresql/18/bin \
--jobs=8 # 新增:并行处理检查
--swap # 新增:交换目录而非复制(节省磁盘 I/O)
# 升级后:统计信息已保留,无需额外 ANALYZE
# 性能立即达到预期水平!
7.3 GIN 并行构建
-- PostgreSQL 18 支持 GIN 索引并行构建
-- 对全文搜索、数组包含查询、JSONB 路径查询有显著提升
-- 大表创建 GIN 索引(PostgreSQL 17 单线程)
CREATE INDEX idx_products_search ON products USING GIN(search_vector);
-- PostgreSQL 18:自动并行构建
-- 内部使用多个 worker 并行处理数据
-- 构建时间减少 60-80%(取决于 CPU 核心数)
SET max_parallel_workers_per_gather = 4;
CREATE INDEX idx_products_search ON products USING GIN(search_vector);
-- 注意:GIN 索引的并行构建有额外的内存开销
SET maintenance_work_mem = '4GB'; -- 建议增大
八、废弃与移除:迁移前必读
8.1 MD5 认证正式废弃
PostgreSQL 18 废弃了 md5 认证方法:
# postgresql.conf
# PostgreSQL 18 警告:
# "md5 authentication method is deprecated and will be removed in a future release"
# 建议迁移到 SCRAM 或 OAuth
-- 检查当前认证方法
SELECT usename, usesuper, usecreatedb
FROM pg_user
WHERE usesuper = true;
-- 查看 pg_hba.conf 中的认证配置
-- 将所有 "md5" 替换为 "scram-sha-256"
-- host all all 0.0.0.0/0 scram-sha-256
8.2 全文搜索重新索引
PostgreSQL 18 修改了全文搜索的默认排序规则提供商,从 libc 改为集群默认提供商。这可能导致升级后需要重建全文搜索索引:
# 升级后检查是否需要重建索引
psql -c "
SELECT relname, relkind
FROM pg_class
WHERE oid IN (
SELECT indexrelid
FROM pg_index
WHERE indisready = false
);
"
# 如果有受影响索引,批量重建
REINDEX INDEX CONCURRENTLY idx_tsvector_1;
REINDEX INDEX CONCURRENTLY idx_tsvector_2;
-- ... 依次重建所有全文搜索和 pg_trgm 索引
九、总结与展望
PostgreSQL 18 的更新可以归结为三个方向:
性能革命:AIO + Skip Scan + GIN 并行
I/O 层:AIO 子系统将 NVMe SSD 的性能释放出来
查询层:Skip Scan 打破多列索引「第一列等值」的限制
索引层:GIN 并行构建让全文搜索不再是大表的噩梦
开发者体验:虚拟列 + UUIDv7 + 时间约束
虚拟生成列让你在「按需计算」和「预计算存储」之间自由切换
UUIDv7 解决了分布式 ID 的性能焦虑
时间约束把业务规则从触发器里解放出来
企业就绪:统计信息跨版本保留 + OAuth 原生认证
升级不再痛苦:统计信息无缝过渡
OAuth 2.0 让 SSO 集成从「配置噩梦」变成「开箱即用」
PostgreSQL 18 标志着开源关系型数据库进入了一个新阶段:不再只是「可靠的存储」,而是「知道数据怎么被访问的智能引擎」。如果你在用 PostgreSQL,升级到 18 是一次几乎不需要付出代价的性能红利;如果你在选型,PostgreSQL 18 进一步巩固了它作为现代应用数据基础设施首选的地位。
唯一需要注意的是:MD5 认证的废弃是一个明确的信号——如果你还在用 MD5,现在就是迁移的最佳时机。别等到下个版本被强制移除时手忙脚乱。