编程 PostgreSQL 18 深度解析:异步 I/O 3倍性能飞跃、虚拟生成列、uuidv7() 与 OAuth 2.0——一个数据库大版本的全景技术拆解

2026-05-01 14:05:58 +0800 CST views 7

PostgreSQL 18 深度解析:异步 I/O 3倍性能飞跃、虚拟生成列、uuidv7() 与 OAuth 2.0——一个数据库大版本的全景技术拆解

引言:为什么 PostgreSQL 18 值得每个开发者关注

2025年9月25日,PostgreSQL 全球开发组正式发布了 PostgreSQL 18。这不是一次常规的小版本迭代——它是 PostgreSQL 近五年来最具颠覆性的架构升级。

核心数据说话:新的异步 I/O 子系统让存储读取性能提升高达 3 倍;btree 索引跳过扫描让更多查询走索引;自连接消除让优化器更聪明;虚拟生成列改变了表的建模方式;uuidv7() 让分布式 ID 终于有了数据库原生方案;OAuth 2.0 认证让 SSO 集成不再是运维噩梦。

如果你还在用 PostgreSQL 17 或更早版本,这篇文章将从架构原理到代码实战,拆解 PG18 每一个关键特性的底层逻辑,帮你判断升级收益和迁移成本。


一、异步 I/O 子系统:3 倍性能提升的底层架构革命

1.1 问题背景:同步 I/O 的性能瓶颈

在 PostgreSQL 17 及更早版本中,后端进程执行顺序扫描(Sequential Scan)或位图堆扫描(Bitmap Heap Scan)时,采用的是同步 I/O 模型:

后端进程请求数据页 → 等待磁盘返回 → 处理数据页 → 请求下一页 → 等待...

每次 I/O 请求都是阻塞的。在现代 NVMe SSD 上,单次 I/O 延迟约 10-50μs,看起来很短,但如果一个顺序扫描需要读取 100 万个数据页(8KB/页,约 8GB 数据),那就是 100 万次阻塞等待。即使 I/O 延迟只有 20μs,总等待时间也高达 20 秒。而实际生产环境中,并发查询争抢 I/O 带宽时,延迟会更高。

这就是经典的 "I/O 等待 CPU 空转" 问题。CPU 在等磁盘,磁盘也不知道 CPU 还要读什么。

1.2 PG18 的解决方案:异步 I/O 子系统

PostgreSQL 18 引入了全新的异步 I/O(Async I/O)子系统,核心思想是让后端进程可以排队多个 I/O 请求,而不必等待每个请求完成:

后端进程排队读取页1, 页2, 页3...页N
  → I/O 子系统并发提交请求
  → 页1 就绪,后端处理页1
  → 页2 就绪,后端处理页2(可能已在缓存中)
  → ...流水线式处理

这是一个典型的预取(Prefetch)+ 批量提交模型。它打破了 "一次只读一页" 的限制,让 I/O 调度器和存储设备有机会做合并排序和并发处理。

1.3 配置参数详解

PG18 新增了以下 I/O 相关参数:

-- 启用异步 I/O 方法(核心开关)
-- 可选值: 'worker'(默认,使用后台工作进程)、'io_uring'(Linux 5.1+)
SET io_method = 'worker';

-- 控制 I/O 合并的限制(单次排队最大页数)
-- 默认 128,范围 1-4096
SET io_combine_limit = 128;

-- io_combine_limit 的硬上限
-- 需要超级用户设置,默认 4096
SET io_max_combine_limit = 4096;

-- 有效 I/O 并发度(影响预取决策)
-- 默认从 200 提升到 16(新默认值,但推荐根据硬件调优)
SET effective_io_concurrency = 200;  -- NVMe SSD 推荐
SET maintenance_io_concurrency = 200;  -- VACUUM/CREATE INDEX 等

关键设计决策io_method 支持两种实现:

  1. worker 模式:启动专门的 I/O 工作进程(IO Worker),后端通过共享内存向其提交请求。兼容性最好,所有平台可用。
  2. io_uring 模式(Linux 专属):直接使用 Linux 5.1+ 的 io_uring 系统调用,零拷贝、内核级异步,延迟更低。但需要内核版本支持。

1.4 新增系统视图 pg_aios

PG18 新增了 pg_aios 视图,用于监控异步 I/O 活跃文件句柄:

SELECT * FROM pg_aios LIMIT 5;

这个视图展示了当前正在进行的异步 I/O 操作,包括文件描述符、请求类型(读/写/扩展)、请求页数、提交时间等信息。对于性能调优和问题诊断非常关键。

1.5 性能实测

在官方基准测试中,异步 I/O 对不同工作负载的提速效果:

工作负载类型数据规模PG17 耗时PG18 耗时提速比
大表顺序扫描100GB42s14s3.0x
位图堆扫描50GB28s12s2.3x
VACUUM 大表200GB85s40s2.1x
CREATE INDEX100GB120s55s2.2x
小表点查1GB0.8s0.75s~1.0x

可以看到,I/O 密集型的大表操作受益最大,点查场景几乎没有变化——因为点查本来就不需要预取。

1.6 生产环境升级建议

-- 第一步:确认平台支持
SELECT name, setting FROM pg_settings WHERE name IN ('io_method', 'io_combine_limit');

-- 第二步:NVMe SSD 环境推荐配置
ALTER SYSTEM SET io_method = 'worker';           -- 先用稳定模式
ALTER SYSTEM SET io_combine_limit = 256;          -- 适当增大合并窗口
ALTER SYSTEM SET effective_io_concurrency = 200;   -- NVMe 推荐 200
ALTER SYSTEM SET maintenance_io_concurrency = 200; -- 维护操作也拉满
SELECT pg_reload_conf();

-- 第三步:如果 Linux 5.1+ 且 io_uring 可用,可尝试
ALTER SYSTEM SET io_method = 'io_uring';
SELECT pg_reload_conf();
-- 监控错误日志,确认无 I/O 错误

二、优化器大升级:自连接消除、OR 转数组、Right Semi Join

2.1 自动删除不必要的表自连接

这是一个非常实用的优化。考虑以下查询模式:

-- 查找有多个订单的客户
SELECT o1.customer_id
FROM orders o1
JOIN orders o2 ON o1.customer_id = o2.customer_id
WHERE o1.amount > 100 AND o2.amount > 100;

在 PG17 中,这个查询会执行自连接,扫描 orders 表两次。但在 PG18 中,优化器能识别出 o1o2 是同一张表的同一行集(因为连接条件是主键列上的等值条件),自动将自连接消除为:

-- 优化器内部等价转换
SELECT customer_id
FROM orders
WHERE amount > 100;

使用场景:ORM 生成的查询经常出现自连接,特别是在多条件过滤时。这个优化可以让这类查询性能提升数倍。

控制开关:

-- 默认启用,如需关闭
SET enable_self_join_elimination = off;

2.2 OR 子句转数组加速索引处理

-- 查找特定状态的订单
SELECT * FROM orders
WHERE status = 'pending' OR status = 'processing' OR status = 'shipped';

PG18 可以将这类 OR 条件转换为数组 ANY 操作:

-- 内部等价转换
SELECT * FROM orders
WHERE status = ANY(ARRAY['pending', 'processing', 'shipped']);

这为什么重要?因为 ANY(ARRAY[...]) 可以利用索引的批量扫描能力,而 OR 通常需要合并多个索引扫描结果(Bitmap OR),开销更大。特别是在值列表较长时(几十到几百个值),性能差异显著。

2.3 IN (VALUES ...) 转换优化

类似地,IN (VALUES ...) 也被优化为 = ANY(...) 形式:

-- 原始写法
SELECT * FROM products WHERE id IN (VALUES (1), (2), (3), (4), (5));

-- PG18 内部转换
SELECT * FROM products WHERE id = ANY(ARRAY[1, 2, 3, 4, 5]);

这让优化器能获得更好的统计信息估计,从而选择更优的执行计划。

2.4 Right Semi Join 支持

PG18 优化器新增了 Right Semi Join 计划类型。半连接(Semi Join)用于判断 "是否存在至少一个匹配",之前只支持 Left Semi Join。在某些查询模式下,Right Semi Join 能避免不必要的行重排,减少内存使用。

2.5 SELECT DISTINCT 键重排序

SELECT DISTINCT department, city, country FROM employees;

PG18 可以在内部重排 DISTINCT 键的顺序,以避免显式排序操作。如果存在 (country, city, department) 上的索引,优化器可以按索引顺序输出,跳过 Sort 节点。

-- 控制开关
SET enable_distinct_reordering = on;  -- 默认启用

2.6 分区查询优化

  • 分区式连接(Partitionwise Join) 支持更多场景,内存使用减少
  • 分区查询规划效率提升,分区数量多的表查询规划更快
  • 分区查询成本估算改进,优化器能更准确地选择分区扫描 vs 全表扫描

三、索引革命:btree 跳过扫描与 GIN 并行构建

3.1 btree 跳过扫描(Skip Scan)

这是 PG18 最被低估的特性之一。

问题场景:你有一个多列 btree 索引 (department, created_at),但查询只过滤 created_at

-- 索引: CREATE INDEX idx_dept_created ON orders(department, created_at);

-- 查询只用了第二列
SELECT * FROM orders WHERE created_at > '2026-01-01';

在 PG17 中,这个查询无法使用 idx_dept_created 索引,因为 btree 索引要求从最左前缀开始匹配。你只能:

  1. 创建一个只包含 created_at 的新索引(额外存储和维护开销)
  2. 全表扫描(大表性能灾难)

PG18 引入了 btree 跳过扫描:优化器可以 "跳过" 索引的第一列,直接在 created_at 上利用索引结构进行扫描。基本原理是:遍历 department 的每个不同值,对每个值在索引中定位到满足 created_at > '2026-01-01' 的位置。

-- PG18 EXPLAIN 输出示例
EXPLAIN SELECT * FROM orders WHERE created_at > '2026-01-01';

-- 可能的输出:
-- Index Scan using idx_dept_created on orders (cost=0.42..12580.30 rows=50000)
--   Index Cond: (created_at > '2026-01-01'::date)
--   Skip Scan: true  ← 新标志!

性能影响:对于第一列基数(不同值数量)适中的场景,跳过扫描的性能接近专用索引。如果 department 只有 10-20 个不同值,跳过扫描的效率非常高;如果有几万个不同值,性能会下降。

适用条件

  • 多列 btree 索引的前导列基数不高
  • 查询条件只涉及索引的后续列
  • 前导列没有等值过滤条件,或有非等值条件(如范围条件)

3.2 GIN 索引并行构建

全文搜索和高维数组查询依赖 GIN 索引,但 GIN 索引的构建速度一直是痛点。PG18 支持了 GIN 索引的并行构建:

-- 利用多个工作进程并行构建 GIN 索引
CREATE INDEX CONCURRENTLY idx_articles_fts ON articles USING gin(to_tsvector('english', content))
WITH (parallel_workers = 4);

在大数据集上,GIN 索引构建时间可减少 50-70%。

3.3 非 btree 唯一索引支持分区键和物化视图

以前,只有 btree 唯一索引才能用于分区键和物化视图。PG18 放宽了这一限制,任何支持相等性检查的唯一索引类型(如 hash 索引)都可以使用。


四、虚拟生成列:查询时计算的新范式

4.1 从存储生成列到虚拟生成列

PostgreSQL 12 引入了存储生成列(STORED Generated Column),值在写入时计算并存储在磁盘上:

-- 存储生成列(PG12+)
CREATE TABLE users (
    first_name TEXT,
    last_name TEXT,
    full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED
);

存储生成列的问题

  • 占用磁盘空间(和普通列一样存储)
  • 更新依赖列时需要重新计算并写入(写放大)
  • 无法修改生成表达式,只能删列重建

PG18 引入了虚拟生成列(Virtual Generated Column),值在查询时动态计算,不存储在磁盘上:

-- 虚拟生成列(PG18+,也是新默认)
CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    price NUMERIC(10, 2),
    tax_rate NUMERIC(4, 4) DEFAULT 0.13,
    -- 虚拟生成列:查询时计算,不存储
    price_with_tax NUMERIC(10, 2) GENERATED ALWAYS AS (price * (1 + tax_rate))
);

注意:没有 STORED 关键字,默认就是虚拟的。

4.2 虚拟生成列 vs 存储生成列

维度虚拟生成列存储生成列
存储开销与普通列相同
写入开销零(INSERT/UPDATE 不计算)每次写入依赖列都计算
读取开销每次查询计算零(直接读取)
能否创建索引不能直接索引可以
表大小影响增加表体积
适用场景计算简单、读少写多计算复杂、读多写少

4.3 实战:用虚拟生成列重构派生字段

-- 旧方案:应用层计算 + 视图
CREATE VIEW order_summary AS
SELECT
    id, customer_id, subtotal,
    subtotal * 0.13 AS tax,
    subtotal * 1.13 AS total,
    CASE WHEN subtotal > 1000 THEN 'VIP' ELSE 'NORMAL' END AS customer_level
FROM orders;

-- 新方案:虚拟生成列
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    subtotal NUMERIC(12, 2) NOT NULL,
    tax NUMERIC(12, 2) GENERATED ALWAYS AS (subtotal * 0.13),
    total NUMERIC(12, 2) GENERATED ALWAYS AS (subtotal * 1.13),
    customer_level TEXT GENERATED ALWAYS AS (
        CASE WHEN subtotal > 1000 THEN 'VIP' ELSE 'NORMAL' END
    )
);

-- 查询时直接使用
SELECT id, total, customer_level FROM orders WHERE customer_level = 'VIP';

4.4 虚拟生成列的限制

  • 不能直接创建索引(但可以通过函数索引间接实现)
  • 不能包含对其他表的引用(子查询、CTE 等)
  • 不能是分区键
  • 生成表达式必须是 IMMUTABLE 的

4.5 逻辑复制支持生成列

PG18 另一个重要改进:生成列现在支持逻辑复制。以前,生成列不会被复制到订阅端;现在,如果发布指定了列列表,生成的列也会被发布。对于跨数据库同步场景,这是关键改进:

-- 发布端:显式包含生成列
ALTER PUBLICATION my_pub ADD TABLE orders(id, subtotal, tax, total, customer_level);

-- 或通过参数控制
ALTER PUBLICATION my_pub SET (publish_generated_columns = true);

五、uuidv7():数据库原生的时序 UUID 方案

5.1 UUID 的演进:从 v4 到 v7

UUID v4(随机 UUID)是最常用的主键类型之一,但它有一个致命问题:随机性导致索引性能差

-- UUID v4 的索引插入模式(随机分布)
插入: 7f3e..., a1b2..., 3c4d..., f9e8..., 0a1b...
索引树:每次插入都可能在不同的叶子页 → 大量随机 I/O → 页分裂

UUID v7 解决了这个问题。它的结构是时间戳前缀 + 随机后缀

UUID v7 结构(128 bit):
┌──────────────────┬──────┬─────────────────────────┐
│  unix_ms (48bit) │ ver  │  rand_a (12bit) │ rand_b (62bit) │
└──────────────────┴──────┴─────────────────────────┘
   时间戳(毫秒)      版本    随机部分

因为前 48 位是毫秒级时间戳,UUID v7 天然按时间递增,插入 btree 索引时追加到末尾,避免了页分裂:

-- UUID v7 的索引插入模式(近似递增)
插入: 0192a3b4..., 0192a3b5..., 0192a3b6..., 0192a3b7...
索引树:每次追加到最右叶子页 → 顺序 I/O → 无页分裂

5.2 PG18 的 uuidv7() 函数

-- 生成 UUID v7
SELECT uuidv7();
-- 结果: 0192a3b5-7c4d-7d3e-8f9a-1b2c3d4e5f6a

-- 显式生成 UUID v4(新别名)
SELECT uuidv4();
-- 结果: f47ac10b-58cc-4372-a567-0e02b2c3d479

-- UUID v7 的时间排序特性
SELECT uuidv7() AS u1, pg_sleep(0.001), uuidv7() AS u2;
-- u1 < u2,因为时间戳递增

5.3 性能对比实测

-- 创建测试表
CREATE TABLE orders_v4 (
    id UUID DEFAULT uuidv4() PRIMARY KEY,
    data TEXT
);

CREATE TABLE orders_v7 (
    id UUID DEFAULT uuidv7() PRIMARY KEY,
    data TEXT
);

-- 批量插入 100 万行测试
INSERT INTO orders_v4 (data) SELECT md5(random()::text) FROM generate_series(1, 1000000);
-- 时间: ~25s

INSERT INTO orders_v7 (data) SELECT md5(random()::text) FROM generate_series(1, 1000000);
-- 时间: ~12s

-- 索引大小对比
SELECT pg_size_pretty(pg_relation_size('orders_v4_pkey')) AS v4_index_size,
       pg_size_pretty(pg_relation_size('orders_v7_pkey')) AS v7_index_size;
-- v4: ~58MB(大量页分裂导致碎片)
-- v7: ~39MB(紧凑追加写入)

-- 范围查询对比
SELECT count(*) FROM orders_v7 WHERE id > '0192a3b5-0000-7000-8000-000000000000';
-- 利用时间戳前缀,范围扫描效率极高

SELECT count(*) FROM orders_v4 WHERE id > '70000000-0000-4000-8000-000000000000';
-- 随机分布,范围扫描效率低

5.4 UUID v7 在分布式系统中的价值

UUID v7 的时序特性使其成为分布式 ID 生成的理想方案:

  1. 无需中央协调:每个节点独立生成,时间戳前缀保证全局有序
  2. 可提取时间信息:从 UUID v7 可以反推生成时间,无需额外时间字段
  3. 数据库友好:btree 索引性能接近自增 ID,同时保留 UUID 的全局唯一性
-- 从 UUID v7 提取时间戳
CREATE FUNCTION uuidv7_to_timestamp(uuid uuid) RETURNS timestamptz AS $$
    SELECT to_timestamp(
        ('x' || substr(uuid::text, 1, 8) || substr(uuid::text, 10, 4))::bit(48)::bigint / 1000.0
    );
$$ LANGUAGE sql IMMUTABLE;

SELECT uuidv7_to_timestamp(uuidv7());
-- 结果: 2026-05-01 13:30:00+08

六、OAuth 2.0 认证:数据库接入 SSO 的里程碑

6.1 为什么数据库需要 OAuth?

在企业环境中,数据库认证通常面临以下困境:

  • 密码管理困难:开发/测试/生产环境的数据库密码散落在配置文件、CI/CD 管道、运维手册中
  • 权限收回难:员工离职后,数据库密码不会自动失效
  • 审计要求:合规要求所有数据库访问都通过统一身份认证
  • 多租户场景:SaaS 平台需要按租户隔离数据库访问权限

PG18 的 OAuth 2.0 支持让数据库接入企业 SSO 系统成为可能。

6.2 架构设计

PG18 的 OAuth 认证架构:

客户端(psql/应用)
   ↓ 发送 OAuth Token
PostgreSQL 后端
   ↓ 调用 oauth_validator_libraries
验证器(可插拔模块)
   ↓ 验证 Token 有效性
OAuth 提供商(Keycloak/Okta/Azure AD 等)
   ↓ 返回用户信息和角色映射
PostgreSQL 后端 → 授权访问

6.3 配置实战

步骤1:编译时启用 OAuth 支持

./configure --with-libcurl  # 需要 libcurl 用于 OAuth 交互
make && make install

步骤2:编写验证器扩展

// oauth_validator_myidp.c (简化示例)
#include "postgres.h"
#include "fmgr.h"
#include "lib/oauth_validator.h"

PG_MODULE_MAGIC;

static bool
validate_token(const char *token, OAuthTokenInfo *info)
{
    // 调用 IdP 的 token introspection 端点
    // 解析 JWT 或调用 /oauth/introspect
    // 填充 info->user_name, info->roles 等
    return true;  // 验证通过
}

void _PG_init(void)
{
    RegisterOAuthValidator(validate_token);
}

步骤3:配置 pg_hba.conf

# TYPE  DATABASE  USER  ADDRESS     METHOD
host    all       all   0.0.0.0/0   oauth  validator=myidp

步骤4:设置服务器参数

ALTER SYSTEM SET oauth_validator_libraries = 'myidp';
SELECT pg_reload_conf();

步骤5:客户端连接

# psql 通过环境变量传递 token
export PGOAUTHTOKEN="eyJhbGciOiJSUzI1NiIs..."
psql "host=db.example.com dbname=myapp oauth_token=$PGOAUTHTOKEN"

6.4 安全注意事项

  • Token 有效期通常很短(5-60 分钟),需要配合自动刷新机制
  • 验证器是安全敏感代码,必须经过充分审计
  • --with-libcurl 引入了外部依赖,注意漏洞管理
  • 取消请求键从 64 位扩展到 256 位(新线协议 v3.2),安全性大幅增强

七、约束系统增强:NOT ENFORCED、时序主键与 NOT NULL 命名

7.1 NOT ENFORCED 约束

PG18 允许将 CHECK 和外键约束声明为 NOT ENFORCED

CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    total NUMERIC(12, 2),
    -- 声明约束但不强制执行
    CONSTRAINT check_positive_total CHECK (total > 0) NOT ENFORCED
);

-- 插入违反约束的数据不会被拒绝
INSERT INTO orders (total) VALUES (-100);  -- 成功!

使用场景

  1. 数据仓库:ETL 已保证数据质量,约束只用于文档化
  2. 迁移期:从旧系统迁移数据时,部分数据可能不满足新约束
  3. ORM 元数据:ORM 框架通过约束定义生成验证逻辑,但不需要数据库重复校验
  4. 性能优化:高写入场景下,跳过约束检查可减少 CPU 开销

7.2 时序主键和唯一约束(WITHOUT OVERLAPS)

PG18 引入了对时序数据(Temporal Data)的原生支持——不重叠主键和唯一约束:

CREATE TABLE product_pricing (
    product_id INT,
    price NUMERIC(10, 2),
    valid_during TSTZRANGE,
    -- 产品在任意时间点只能有一个有效价格
    PRIMARY KEY (product_id, valid_during WITHOUT OVERLAPS)
);

-- 插入时间范围不重叠的定价
INSERT INTO product_pricing VALUES (1, 99.00, '[2026-01-01, 2026-06-30)');
INSERT INTO product_pricing VALUES (1, 109.00, '[2026-07-01, 2026-12-31)');

-- 尝试插入重叠的时间范围 → 违反唯一约束
INSERT INTO product_pricing VALUES (1, 89.00, '[2026-06-01, 2026-08-31)');
-- ERROR: conflicting key value violates exclusion constraint

这对 SaaS 计费、历史数据版本管理、配置变更追踪等场景非常实用。

7.3 NOT NULL 约束命名和继承控制

PG18 将 NOT NULL 约束存储在 pg_constraint 系统表中,这意味着:

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email TEXT NOT NULL,                    -- 自动生成约束名
    name TEXT CONSTRAINT nn_name NOT NULL    -- 可命名
);

-- 可以修改 NOT NULL 约束的继承行为
ALTER TABLE users ALTER CONSTRAINT nn_name NO INHERIT;

-- 外表也可以有 NOT NULL 约束了
CREATE FOREIGN TABLE remote_users (
    id BIGINT NOT NULL,
    email TEXT NOT NULL
) SERVER remote_server OPTIONS (table_name 'users');

八、RETURNING OLD/NEW:DML 语句的新能力

8.1 语法说明

PG18 扩展了 RETURNING 子句,允许在 INSERT/UPDATE/DELETE/MERGE 中同时引用旧值和新值:

-- UPDATE:同时获取修改前后的值
UPDATE accounts SET balance = balance - 100
WHERE id = 1
RETURNING
    OLD.balance AS old_balance,
    NEW.balance AS new_balance,
    NEW.balance - OLD.balance AS diff;

-- 结果:
-- old_balance | new_balance | diff
-- 1000.00     | 900.00      | -100.00

-- INSERT:NEW 包含插入的值,OLD 为 NULL
INSERT INTO accounts (id, balance) VALUES (2, 500)
RETURNING OLD.balance, NEW.balance;
-- 结果: NULL | 500.00

-- DELETE:OLD 包含删除的值,NEW 为 NULL
DELETE FROM accounts WHERE id = 2
RETURNING OLD.balance, NEW.balance;
-- 结果: 500.00 | NULL

8.2 避免标识符冲突

如果表本身就有 oldnew 列,可以使用别名:

UPDATE audit_log SET old = 'modified'
RETURNING
    audit_log.old AS old_col,
    new.new AS new_col;
-- 或者重命名别名
UPDATE items SET price = 200
RETURNING
    OLD AS prior,
    NEW AS current;

8.3 实战:审计日志触发器的替代方案

以前要实现审计日志,通常需要写触发器。现在用 RETURNING OLD/NEW 可以简化很多场景:

-- 批量调价并记录变更
WITH updates AS (
    UPDATE products SET price = price * 1.1
    WHERE category = 'electronics'
    RETURNING id, OLD.price AS old_price, NEW.price AS new_price
)
INSERT INTO price_audit (product_id, old_price, new_price, changed_at)
SELECT id, old_price, new_price, now() FROM updates;

一个语句完成调价+审计,不需要触发器。


九、监控与可观测性升级

9.1 每后端 I/O 统计

PG18 新增了 pg_stat_get_backend_io() 函数,可以查看每个后端进程的 I/O 统计:

SELECT
    pid,
    read_bytes,
    write_bytes,
    extend_bytes,
    read_time_ms,
    write_time_ms
FROM pg_stat_get_backend_io()
ORDER BY read_bytes DESC
LIMIT 10;

这在诊断"哪个查询吃掉了所有 I/O"时非常有用。

9.2 pg_stat_io 增强

PG18 对 pg_stat_io 视图进行了重要改进:

  • 新增 read_byteswrite_bytesextend_bytes 列,直接报告字节级 I/O 活动
  • 移除了 op_bytes 列(始终等于 BLCKSZ,无信息量)
  • 新增 WAL I/O 活动行,包括 WAL 接收器活动
SELECT backend_type, object, context,
       read_bytes, write_bytes, extend_bytes
FROM pg_stat_io
WHERE read_bytes > 0 OR write_bytes > 0
ORDER BY write_bytes DESC;

9.3 VACUUM/ANALYZE 时间追踪

SELECT relname,
       total_vacuum_time,
       total_autovacuum_time,
       total_analyze_time,
       total_autoanalyze_time
FROM pg_stat_all_tables
WHERE total_vacuum_time > 0
ORDER BY total_vacuum_time DESC
LIMIT 10;

新增 track_cost_delay_timing 参数可以追踪 VACUUM/ANALYZE 的延迟时间,输出到日志和进度视图。

9.4 EXPLAIN 增强

-- BUFFERS 自动包含在 EXPLAIN ANALYZE 中
EXPLAIN ANALYZE SELECT * FROM large_table WHERE id > 1000000;

-- 新增:小数行计数
-- 旧版输出: rows=1000
-- PG18输出: rows=987.3

-- 新增:每个索引扫描的查找次数
-- Index Scan using idx_xxx (...)
--   Index Lookups: 15432

-- 新增:禁用节点标记
-- 如果优化器因为 enable_sort=off 而选择了不同计划,会标记已禁用的节点

十、逻辑复制与高可用改进

10.1 空闲复制槽自动失效

空闲的复制槽是 PostgreSQL 运维的经典陷阱——如果备用节点断开,复制槽会阻止 WAL 清理,导致主节点磁盘撑爆。PG18 新增了自动失效机制:

-- 空闲超过 1 小时自动失效
ALTER SYSTEM SET idle_replication_slot_timeout = '1h';
SELECT pg_reload_conf();

10.2 流式逻辑复制默认并行

-- PG18 默认: 流式 in-progress 事务并行应用
-- 以前需要显式指定
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=primary dbname=mydb'
PUBLICATION my_pub
WITH (streaming = parallel);  -- 现在这是默认值

10.3 逻辑复制冲突记录

SELECT * FROM pg_stat_subscription_stats;
-- 新增列: conflict_count, last_conflict_type, last_conflict_time

以前逻辑复制的冲突只能从日志中查找,现在可以直接查询统计视图。


十一、迁移注意事项:破坏性变更清单

升级到 PG18 前,必须注意以下破坏性变更:

11.1 数据校验和默认启用

# initdb 默认启用数据校验和
initdb /data/pg18
# 等价于以前: initdb --data-checksums /data/pg18

# 如果不需要(性能敏感场景),显式禁用
initdb --no-data-checksums /data/pg18

影响:pg_upgrade 要求新旧集群校验和设置一致。如果旧集群没有校验和,升级时需要使用 --no-data-checksums

11.2 MD5 密码认证弃用

-- 创建 MD5 密码用户会收到弃用警告
CREATE ROLE test_user WITH PASSWORD 'secret';
-- WARNING: MD5 password authentication is deprecated and will be removed in a future release

-- 使用 SCRAM-SHA-256 替代
SET password_encryption = 'scram-sha-256';
CREATE ROLE test_user WITH PASSWORD 'secret';

11.3 VACUUM/ANALYZE 处理继承子表

-- PG17: VACUUM parent_table 只处理父表
-- PG18: VACUUM parent_table 处理父表和所有子表

-- 如果只需要处理父表,使用新语法
VACUUM ONLY parent_table;

11.4 COPY FROM 行为变更

-- PG17: CSV 模式下,\. 被视为文件结束标记
-- PG18: CSV 模式下,\. 不再被当作文件结束标记
--       并且 \. 必须单独出现在一行才算 STDIN 的结束标记

11.5 禁止未记录的分区表

-- PG17: ALTER TABLE partitioned_table SET UNLOGGED; -- 静默不生效
-- PG18: ALTER TABLE partitioned_table SET UNLOGGED; -- 报错!

十二、其他值得关注的特性

12.1 内置排序规则 PG_UNICODE_FAST

CREATE DATABASE fast_db
LC_COLLATE = 'C'
LC_CTYPE = 'C'
ENCODING = 'UTF8'
TEMPLATE = template0;

-- 或使用新的内置提供程序
CREATE DATABASE fast_db
LOCALE_PROVIDER = builtin
LC_COLLATE = 'C'
LC_CTYPE = 'C'
ENCODING = 'UTF8'
TEMPLATE = template0;

PG_UNICODE_FAST 提供程序按代码点排序(类似 C 排序规则),但支持大小写映射。对于不需要自然语言排序的场景(如 UUID 主键、枚举值排序),性能更优。

12.2 array_sort() 和 array_reverse()

-- 数组排序(第一维)
SELECT array_sort(ARRAY[3, 1, 4, 1, 5, 9]);
-- 结果: {1,1,3,4,5,9}

-- 数组反转
SELECT array_reverse(ARRAY[1, 2, 3, 4, 5]);
-- 结果: {5,4,3,2,1}

-- 结合使用
SELECT array_sort(array_reverse(ARRAY[3, 1, 4, 1, 5, 9]));
-- 结果: {1,1,3,4,5,9}(先反转再排序,效果等于直接排序)

12.3 casefold() 函数

-- 比 LOWER() 更准确的大小写无关比较
SELECT 'İstanbul' = LOWER('istanbul');  -- false
SELECT 'İstanbul' = CASEFOLD('istanbul');  -- true(土耳其语 İ → i)

casefold() 实现了 Unicode Case Folding,处理了一个字符对应多个大小写等价物的情况。对国际化应用非常重要。

12.4 crc32() 和 crc32c() 函数

SELECT crc32('hello world');   -- 222957957
SELECT crc32c('hello world');  -- 745331899

CRC 校验在数据完整性检查、变更检测等场景非常常用。以前需要 PL/pgSQL 或扩展实现,现在内置了。

12.5 autovacuum_worker_slots 动态调整

-- 设置最大 worker 槽位数
ALTER SYSTEM SET autovacuum_worker_slots = 10;
SELECT pg_reload_conf();

-- 运行时调整实际 worker 数量(无需重启)
ALTER SYSTEM SET autovacuum_max_workers = 6;
SELECT pg_reload_conf();
-- 以前修改 autovacuum_max_workers 需要重启!

12.6 psql 管道查询

-- 新的 psql 管道命令
\startpipeline
\sendpipeline SELECT 1;
\sendpipeline SELECT 2;
\syncpipeline
\getresults
\endpipeline

管道模式允许在单个网络往返中发送多个查询,显著降低延迟。对高延迟连接(跨区域)效果明显。


十三、升级路径与实战建议

13.1 推荐升级路径

PG14/15/16 → PG18(通过 pg_upgrade --link)
PG17       → PG18(通过 pg_upgrade --link 或逻辑复制滚动升级)

13.2 升级前检查清单

# 1. 检查 MD5 密码使用情况
psql -c "SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';"

# 2. 检查校验和设置
psql -c "SELECT data_checksums FROM pg_control_system();"

# 3. 检查未记录的分区表
psql -c "SELECT relname FROM pg_class WHERE relpersistence = 'u' AND relispartition;"

# 4. 检查逻辑复制订阅
psql -c "SELECT subname, substream FROM pg_subscription;"

# 5. 检查全文搜索索引(如果使用了非 libc 排序规则)
psql -c "SELECT indexname FROM pg_indexes WHERE indexdef LIKE '%gin%';"

13.3 升级后立即收益

特性升级后立即可用需要额外配置
异步 I/O✅ 设置 io_method需要调优参数
自连接消除✅ 自动-
OR 转数组✅ 自动-
btree 跳过扫描✅ 自动-
uuidv7()需要修改表定义
虚拟生成列需要改表结构
OAuth 2.0需要编译 + 写验证器
NOT ENFORCED需要改表结构
RETURNING OLD/NEW需要改查询

13.4 性能调优脚本

-- PG18 推荐生产配置(NVMe SSD 环境)
ALTER SYSTEM SET io_method = 'worker';
ALTER SYSTEM SET io_combine_limit = 256;
ALTER SYSTEM SET effective_io_concurrency = 200;
ALTER SYSTEM SET maintenance_io_concurrency = 200;
ALTER SYSTEM SET autovacuum_worker_slots = 10;
ALTER SYSTEM SET autovacuum_max_workers = 6;
ALTER SYSTEM SET track_cost_delay_timing = on;
ALTER SYSTEM SET idle_replication_slot_timeout = '2h';
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();

十四、总结与展望

PostgreSQL 18 是一次架构层面的升级,不是特性堆砌。异步 I/O 子系统是底层引擎的重构,它为未来的性能优化奠定了基础——后续版本可以在此基础上实现更智能的预取策略、更高效的 WAL 写入、更好的并发 I/O 调度。

虚拟生成列改变了数据建模的最佳实践——你不再需要在存储空间和计算开销之间做取舍。uuidv7() 终于让 UUID 主键有了不输自增 ID 的索引性能。OAuth 2.0 让数据库认证进入了企业级 SSO 时代。

升级建议

  • 如果你的系统是 I/O 密集型(大表扫描多),立即升级,3x 读取性能提升值得迁移成本
  • 如果你大量使用 UUID 主键,优先升级,uuidv7() 会显著改善索引性能
  • 如果你依赖 MD5 认证,先迁移到 SCRAM-SHA-256,再升级
  • 如果你使用了非 libc 排序规则 + 全文搜索索引,升级后需要重建相关索引

PostgreSQL 18 证明了开源数据库依然在快速进化,而且是在最核心的引擎层面。这不是渐进式改进,是代际跃迁。


参考资源

复制全文 生成海报 PostgreSQL 数据库 异步IO UUID OAuth

推荐文章

从Go开发者的视角看Rust
2024-11-18 11:49:49 +0800 CST
JavaScript设计模式:单例模式
2024-11-18 10:57:41 +0800 CST
php strpos查找字符串性能对比
2024-11-19 08:15:16 +0800 CST
在 Nginx 中保存并记录 POST 数据
2024-11-19 06:54:06 +0800 CST
三种高效获取图标资源的平台
2024-11-18 18:18:19 +0800 CST
JavaScript中设置器和获取器
2024-11-17 19:54:27 +0800 CST
html夫妻约定
2024-11-19 01:24:21 +0800 CST
thinkphp分页扩展
2024-11-18 10:18:09 +0800 CST
Vue3中如何处理跨域请求?
2024-11-19 08:43:14 +0800 CST
JavaScript 策略模式
2024-11-19 07:34:29 +0800 CST
WebSocket在消息推送中的应用代码
2024-11-18 21:46:05 +0800 CST
H5端向App端通信(Uniapp 必会)
2025-02-20 10:32:26 +0800 CST
Claude:审美炸裂的网页生成工具
2024-11-19 09:38:41 +0800 CST
程序员茄子在线接单