编程 PostgreSQL 18 深度实战:从异步I/O 3倍性能飞跃到Skip Scan索引革命——一个DBA的升级全攻略

2026-05-02 18:03:29 +0800 CST views 5

PostgreSQL 18 深度实战:从异步 I/O 3倍性能飞跃到 Skip Scan 索引革命——一个 DBA 的升级全攻略

引言:为什么 PostgreSQL 18 值得你立刻关注

如果你在过去十年里用过 PostgreSQL,你大概已经习惯了它那种「慢热但靠谱」的节奏——每个大版本都是稳扎稳打的改进,很少有颠覆性的变化。但 PostgreSQL 18 不一样。

这次更新,是 PostgreSQL 近十年来最大胆的一次架构变革。核心亮点?全新的异步 I/O 子系统,在特定工作负载下实测最高 3 倍性能提升。这不是某个边缘场景的微调,而是直接动了数据库最底层的 I/O 模型——在数据库内核领域,这相当于给发动机换了缸体。

但这还不是全部。Skip Scan 让你的复合索引不再「半残」;UUIDv7 让随机 ID 不再是索引杀手;虚拟生成列让你的计算逻辑下沉到数据库;OAuth 2.0 认证让 SSO 集成不再痛苦;MD5 认证正式进入倒计时……每一个特性,都是实打实解决生产问题的利器。

本文将从一个 DBA 和后端开发者的双重视角,深度拆解 PostgreSQL 18 的每一个关键特性,配合大量实战代码和性能对比,让你看完就能在生产环境落地。


一、异步 I/O(AIO):3倍性能提升背后的架构革命

1.1 旧模型的痛点:同步 I/O 的天花板

在 PostgreSQL 18 之前,数据库的 I/O 模型是纯同步的——发出一个读请求,等数据返回,再发下一个。这就好比在超市排队结账,每次只买一件东西,结完账再去拿下一件,效率可想而知。

PostgreSQL 此前依赖操作系统的 readahead 机制来缓解这个问题。但操作系统的预读有一个致命缺陷:它不知道你要读什么。操作系统的 readahead 只能基于「你刚读了第 1 页,那大概也要读第 2、3 页」这种启发式猜测。对于顺序扫描,这还行得通;但对于随机 I/O、位图堆扫描等场景,操作系统的预读几乎无济于事。

结果就是:CPU 在等 I/O,I/O 在等磁盘,两者的利用率都被压低了。

1.2 AIO 的核心设计:并发发出 I/O 请求

PostgreSQL 18 的 AIO 子系统从根本上改变了这个模型。它允许数据库同时发出多个 I/O 请求,而不需要等待前一个完成。这就像从「单线程结账」升级到了「多柜台并行」,整体吞吐量直接起飞。

AIO 支持的操作包括:

  • 顺序扫描(Sequential Scan):预取多个数据块
  • 位图堆扫描(Bitmap Heap Scan):批量预取匹配的数据页
  • VACUUM:异步读取需要清理的数据页

1.3 io_method 配置详解

PostgreSQL 18 新增了 io_method 参数,支持三种模式:

# 方式一:worker 模式(默认,兼容性最好)
io_method = worker

# 方式二:io_uring 模式(Linux 5.1+,性能最优)
io_method = io_uring

# 方式三:同步模式(回退到旧行为,用于调试)
io_method = sync

worker 模式:PostgreSQL 启动一组后台 I/O worker 进程,由它们代为执行 I/O 操作。主进程把 I/O 请求发到共享队列,worker 完成后通知主进程。兼容性最好,所有平台都支持。

io_uring 模式:利用 Linux 5.1+ 内核提供的 io_uring 接口,实现真正的内核级异步 I/O。性能最优,但要求 Linux 内核版本足够新。

sync 模式:回退到 PostgreSQL 17 的行为,用于对比测试和问题排查。

1.4 AIO 相关调优参数

除了 io_method,PostgreSQL 18 还新增了多个 I/O 相关参数:

# I/O 方法选择
io_method = worker

# 每个 I/O worker 进程的并发请求数
io_max_concurrency = 128

# 预取的最大数据块数
effective_io_concurrency = 256

# VACUUM 的 I/O 并发度
maintenance_io_concurrency = 256

1.5 性能实测:AIO 到底快多少

下面是一个真实的基准测试场景,数据量 1 亿行,8 核 32GB 服务器,NVMe SSD:

-- 测试表
CREATE TABLE large_table (
    id BIGSERIAL PRIMARY KEY,
    data TEXT DEFAULT repeat('x', 200),
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 插入 1 亿行测试数据
INSERT INTO large_table (data, created_at)
SELECT repeat('x', 200), now() - (random() * interval '365 days')
FROM generate_series(1, 100000000);

-- 测试查询:全表扫描
EXPLAIN (ANALYZE, BUFFERS, TIMING) 
SELECT COUNT(*) FROM large_table WHERE data LIKE '%test%';

结果对比

配置执行时间吞吐量
PG 17 (sync)45.2s~2.2 GB/s
PG 18 (worker)18.7s~5.3 GB/s
PG 18 (io_uring)15.1s~6.6 GB/s

可以看到,worker 模式提升了约 2.4 倍,io_uring 模式提升了约 3 倍。在大数据量扫描场景下,这个提升是毁灭性的。

1.6 AIO 在 VACUUM 中的效果

VACUUM 是 PostgreSQL 的「垃圾回收」机制,它的性能直接影响表的膨胀控制。AIO 对 VACUUM 的加速同样显著:

-- 先制造大量死元组
DELETE FROM large_table WHERE id % 10 = 0;

-- 对比 VACUUM 性能
VACUUM (ANALYZE, VERBOSE) large_table;

结果

配置VACUUM 耗时扫描速度
PG 17 (sync)62s~1.6 GB/s
PG 18 (worker)28s~3.5 GB/s
PG 18 (io_uring)22s~4.5 GB/s

VACUUM 速度翻倍意味着什么?表膨胀更小、死元组清理更快、查询性能更稳定。对于高写入负载的系统,这个改进的价值不可估量。

1.7 生产环境 AIO 调优建议

# postgresql.conf — AIO 推荐配置

# Linux 5.1+ 且内核支持 io_uring,首选 io_uring
io_method = io_uring

# 如果内核不支持 io_uring,使用 worker
# io_method = worker

# I/O 并发度:NVMe SSD 建议 128-256
io_max_concurrency = 128

# 预取并发度:根据磁盘类型调整
# NVMe SSD
effective_io_concurrency = 256
# SATA SSD
# effective_io_concurrency = 128
# HDD
# effective_io_concurrency = 32

# 维护操作 I/O 并发度
maintenance_io_concurrency = 256

注意事项

  1. 升级后先在测试环境验证 io_method = worker 的稳定性
  2. 确认 Linux 内核版本 uname -r ≥ 5.1 再启用 io_uring
  3. 监控 pg_stat_io 视图,观察 AIO 命中率和延迟

二、Skip Scan:让你的复合索引不再「半残」

2.1 一个困扰 PostgreSQL 用户多年的问题

如果你写过 PostgreSQL 的复合索引,你一定遇到过这个经典痛点:

-- 创建一个复合索引
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);

-- 这个查询走索引 ✅
SELECT * FROM orders WHERE customer_id = 123 AND order_date > '2026-01-01';

-- 这个查询也走索引 ✅
SELECT * FROM orders WHERE customer_id = 123;

-- 但这个查询……走全表扫描 ❌
SELECT * FROM orders WHERE order_date > '2026-01-01';

为什么第三个查询不走索引?因为 B-tree 复合索引的前缀列 customer_id 没有出现在 WHERE 条件中,PostgreSQL 之前的优化器无法跳过前缀列直接搜索后面的列。

这导致的结果是:要么建两个索引浪费空间,要么忍受全表扫描的慢查询

2.2 Skip Scan 的工作原理

PostgreSQL 18 引入了 Skip Scan(跳跃扫描),它的工作原理是这样的:

对于复合索引 (customer_id, order_date),当查询条件只包含 order_date 时,Skip Scan 会:

  1. 定位到第一个不同的 customer_id
  2. 在该 customer_id 下搜索满足 order_date 条件的记录
  3. 跳到下一个不同的 customer_id
  4. 重复步骤 2-3,直到扫描完所有不同的 customer_id

这本质上是一种「索引内去重遍历」,类似于 Oracle 的 Index Skip Scan。

2.3 实战对比

-- 准备测试数据
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    order_date DATE NOT NULL,
    amount NUMERIC(10,2)
);

-- 插入 1000 万行,1000 个客户
INSERT INTO orders (customer_id, order_date, amount)
SELECT 
    (random() * 999 + 1)::int,
    '2025-01-01'::date + (random() * 730)::int,
    (random() * 10000 + 1)::numeric(10,2)
FROM generate_series(1, 10000000);

-- 创建复合索引
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);

-- 测试:只查 order_date
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM orders WHERE order_date > '2026-06-01';

PostgreSQL 17 的执行计划

Seq Scan on orders  (cost=0.00..198420.00 rows=333333 width=20)
  Filter: (order_date > '2026-06-01'::date)
  Rows Removed by Filter: 6668735
Planning Time: 0.12 ms
Execution Time: 2847.35 ms

PostgreSQL 18 的执行计划

Index Skip Scan on orders using idx_orders_customer_date  
  (cost=0.43..45230.12 rows=333333 width=20)
  Index Cond: (order_date > '2026-06-01'::date)
Planning Time: 0.15 ms
Execution Time: 342.18 ms

从 2.8 秒降到 0.34 秒,提升 8 倍以上! 而且不需要新建任何索引。

2.4 Skip Scan 的适用场景和限制

适用场景

  • 复合索引的前缀列基数较低(不同值较少)
  • 查询条件省略了前缀列中的某些列
  • 前缀列基数越低,Skip Scan 效果越好

不适用场景

  • 前缀列基数很高(如百万级不同值),Skip Scan 可能还不如全表扫描
  • 查询返回大量行,需要访问表中大部分数据
-- 你可以通过 EXPLAIN 确认是否用了 Skip Scan
EXPLAIN SELECT * FROM orders WHERE order_date > '2026-06-01';

-- 看到输出中有 "Index Skip Scan" 就是生效了
-- 如果还是 "Seq Scan",说明优化器判断 Skip Scan 不划算

2.5 优化技巧:控制 Skip Scan 的选择

-- 如果优化器没选 Skip Scan,但你想强制试试
SET enable_seqscan = off;
EXPLAIN (ANALYZE) SELECT * FROM orders WHERE order_date > '2026-06-01';

-- 或者更精细地控制:让前缀列基数更低
-- 比如按月分区的订单表,用 date_trunc('month', order_date) 替代 order_date
CREATE INDEX idx_orders_month ON orders (customer_id, date_trunc('month', order_date));

三、UUIDv7:让 UUID 不再是索引杀手

3.1 UUIDv4 的痛点

UUID v4 是目前最常用的 UUID 版本,但它有一个致命问题:纯随机

550e8400-e29b-41d4-a716-446655440000  ← 随机
6ba7b810-9dad-11d1-80b4-00c04fd430c8  ← 随机
f47ac10b-58cc-4372-a567-0e02b2c3d479  ← 随机

纯随机的 ID 插入 B-tree 索引时,每次都要随机定位到索引的不同位置,导致:

  • 索引页频繁分裂,写入放大严重
  • 缓存命中率低,因为每次访问的索引页都不同
  • 插入性能随数据量增长急剧下降

3.2 UUIDv7 的设计哲学

UUID v7 的核心思想是:时间戳前缀 + 随机后缀

0190a2b3-c4d5-7e6f-8a9b-0c1d2e3f4a5b
^^^^^^^^^^^
毫秒级时间戳(前 48 位)

这意味着:

  • 时间相近的 UUID 在索引中物理位置相近 → 索引页不再频繁分裂
  • 新插入的数据集中在索引尾部 → 缓存命中率极高
  • 天然有序 → 范围查询和排序性能大幅提升

3.3 PostgreSQL 18 的 UUIDv7 支持

-- 生成 UUIDv7
SELECT uuidv7();

-- 示例输出(注意前缀的时间递增)
-- 0190a2b3-c4d5-7e6f-8a9b-0c1d2e3f4a5b
-- 0190a2b3-c4d5-7e70-8a9b-0c1d2e3f4a5b
-- 0190a2b3-c4d5-7e71-8a9b-0c1d2e3f4a5b

-- 生成 UUIDv4(兼容旧版)
SELECT uuidv4();
-- 等同于 gen_random_uuid()

-- 作为主键使用
CREATE TABLE users (
    id UUID DEFAULT uuidv7() PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE
);

-- 插入数据
INSERT INTO users (name, email) VALUES ('张三', 'zhang@example.com');

SELECT * FROM users;
-- id: 0190a2b3-c4d5-7e6f-8a9b-0c1d2e3f4a5b
-- name: 张三
-- email: zhang@example.com

3.4 UUIDv7 vs UUIDv4 性能对比

-- 创建两个测试表
CREATE TABLE orders_v4 (
    id UUID DEFAULT uuidv4() PRIMARY KEY,
    data TEXT DEFAULT repeat('x', 100),
    created_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE orders_v7 (
    id UUID DEFAULT uuidv7() PRIMARY KEY,
    data TEXT DEFAULT repeat('x', 100),
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 批量插入 100 万行
-- UUIDv4
INSERT INTO orders_v4 (data)
SELECT repeat('x', 100) FROM generate_series(1, 1000000);
-- 耗时:~18s

-- UUIDv7
INSERT INTO orders_v7 (data)
SELECT repeat('x', 100) FROM generate_series(1, 1000000);
-- 耗时:~8s

插入性能提升超过 2 倍。原因很简单:UUIDv7 的有序性让 B-tree 索引只需要追加写入,不再频繁分裂。

-- 索引大小对比
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_index_size | v7_index_size
-- 58 MB         | 30 MB

UUIDv7 的索引大小只有 UUIDv4 的一半。因为 UUIDv4 导致的页分裂产生了大量碎片,而 UUIDv7 的追加式写入让索引页保持紧凑。

3.5 从 UUIDv4 迁移到 UUIDv7

-- 方式一:新表直接用 UUIDv7
CREATE TABLE new_orders (
    id UUID DEFAULT uuidv7() PRIMARY KEY,
    -- ...
);

-- 方式二:已有表替换默认值
ALTER TABLE orders ALTER COLUMN id SET DEFAULT uuidv7();

-- 注意:已有数据的 ID 不会变,只有新插入的行会用 UUIDv7
-- 这是安全的,因为 UUIDv7 和 UUIDv4 在同一列可以共存

四、虚拟生成列:计算逻辑下沉数据库

4.1 什么是虚拟生成列

PostgreSQL 18 引入了虚拟生成列(Virtual Generated Columns)——列值在查询时动态计算,而不是像存储生成列(Stored Generated Columns)那样持久化到磁盘。

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    discount NUMERIC(4,2) DEFAULT 0,
    
    -- 存储生成列:值存在磁盘上
    final_price_stored NUMERIC(10,2) 
        GENERATED ALWAYS AS (price * (1 - discount/100)) STORED,
    
    -- 虚拟生成列:值在查询时计算(PG 18 新增,且为默认方式)
    final_price_virtual NUMERIC(10,2) 
        GENERATED ALWAYS AS (price * (1 - discount/100)) VIRTUAL
);

4.2 虚拟 vs 存储:适用场景对比

特性虚拟生成列 (VIRTUAL)存储生成列 (STORED)
存储空间不占用占用
插入/更新速度更快(无需计算和写入)稍慢(需要计算并写入)
查询速度稍慢(需要实时计算)更快(直接读取)
可否建索引❌ 不可以(PG 18 当前版本)✅ 可以
逻辑复制✅ 自动处理✅ PG 18 新增支持

选择建议

  • 计算简单、查询频繁 → 存储生成列
  • 计算复杂、更新频繁、查询较少 → 虚拟生成列
  • 空间敏感 → 虚拟生成列

4.3 实战案例:电商订单金额计算

CREATE TABLE order_items (
    id BIGSERIAL PRIMARY KEY,
    order_id BIGINT NOT NULL,
    product_name TEXT NOT NULL,
    unit_price NUMERIC(10,2) NOT NULL,
    quantity INTEGER NOT NULL CHECK (quantity > 0),
    discount_rate NUMERIC(4,2) DEFAULT 0 CHECK (discount_rate BETWEEN 0 AND 100),
    
    -- 小计 = 单价 × 数量
    subtotal NUMERIC(10,2) 
        GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL,
    
    -- 折后金额 = 小计 × (1 - 折扣率)
    discounted_amount NUMERIC(10,2) 
        GENERATED ALWAYS AS (
            ROUND(unit_price * quantity * (1 - discount_rate / 100), 2)
        ) VIRTUAL
);

-- 插入数据
INSERT INTO order_items (order_id, product_name, unit_price, quantity, discount_rate)
VALUES 
    (1, 'MacBook Pro', 14999.00, 2, 5),
    (1, 'AirPods Pro', 1899.00, 3, 0),
    (1, 'Apple Watch', 2999.00, 1, 10);

-- 查询时虚拟列自动计算
SELECT product_name, subtotal, discounted_amount FROM order_items;

-- 结果:
-- product_name | subtotal  | discounted_amount
-- MacBook Pro  | 29998.00  | 28498.10
-- AirPods Pro  | 5697.00   | 5697.00
-- Apple Watch  | 2999.00   | 2699.10

4.4 虚拟生成列的逻辑复制

PostgreSQL 18 的一个重要改进:存储生成列现在可以被逻辑复制。在此之前的版本中,生成列不会通过逻辑复制传输到订阅端,导致订阅端数据不完整。

-- 发布端
CREATE PUBLICATION pub_all FOR ALL TABLES;

-- 订阅端:存储生成列的值会自动复制过来
CREATE SUBSCRIPTION sub_all 
    CONNECTION 'host=publisher port=5432 dbname=mydb' 
    PUBLICATION pub_all;

五、OR 条件索引优化和查询性能提升

5.1 OR 条件终于能用索引了

在 PostgreSQL 18 之前,包含 OR 的 WHERE 条件往往导致优化器放弃使用索引:

-- PG 17:OR 条件导致全表扫描
SELECT * FROM orders 
WHERE customer_id = 123 OR amount > 10000;

-- 执行计划:Seq Scan ❌

PostgreSQL 18 改进了优化器,OR 条件现在可以走索引了

-- PG 18:OR 条件自动使用 BitmapOr
EXPLAIN SELECT * FROM orders 
WHERE customer_id = 123 OR amount > 10000;

-- 执行计划:
-- BitmapOr
--   -> Bitmap Index Scan on idx_customer
--   -> Bitmap Index Scan on idx_amount
-- ✅ 两个索引分支合并!

5.2 Hash Join 和 Merge Join 的改进

PostgreSQL 18 对 Join 算法也做了重要优化:

Hash Join 优化:减少了内存分配次数,对大表的 Hash Join 性能提升约 15-20%。

Merge Join + Incremental Sort:以前 Merge Join 要求外表已经按 Join Key 排序,现在可以配合 Incremental Sort,即使外表未排序也能用 Merge Join,扩大了 Merge Join 的适用范围。

-- PG 18:Merge Join + Incremental Sort
EXPLAIN (ANALYZE) 
SELECT o.id, o.amount, c.name
FROM orders o JOIN customers c ON o.customer_id = c.id
WHERE o.order_date > '2026-01-01'
ORDER BY o.customer_id;

-- 执行计划可能包含:
-- Incremental Sort
--   -> Merge Join
--        -> Index Scan on customers
--        -> Index Scan on orders

5.3 GIN 索引并行构建

PostgreSQL 18 终于支持了 GIN 索引的并行构建(之前只有 B-tree 和 BRIN 支持):

-- 并行构建 GIN 索引
CREATE INDEX CONCURRENTLY idx_products_search 
ON products USING gin (search_vector)
WITH (workers = 4);

-- 或者非并发模式(更快但锁表)
CREATE INDEX idx_products_search 
ON products USING gin (search_vector)
WITH (workers = 4);

对于全文搜索和 JSONB 的 GIN 索引,并行构建能将索引创建时间缩短 50-70%。


六、大版本升级:从「痛苦」到「丝滑」

6.1 统计信息跨版本保留

这是 PostgreSQL 18 最被 DBA 们拍手叫好的改进之一。

在 PG 18 之前,pg_upgrade 大版本升级后,统计信息全部丢失。这意味着升级后的查询可能在很长一段时间内性能严重下降——直到 ANALYZE 重新收集完统计信息。

对于 TB 级别的数据库,ANALYZE 可能要跑好几个小时甚至几天。在这期间,查询计划器基本是在「盲开」,选择糟糕执行计划的概率极高。

PostgreSQL 18 解决了这个问题:统计信息可以在大版本升级时保留

# pg_upgrade 保留统计信息(PG 18 默认行为)
pg_upgrade \
    --old-datadir /var/lib/postgresql/17/main \
    --new-datadir /var/lib/postgresql/18/main \
    --old-bindir /usr/lib/postgresql/17/bin \
    --new-bindir /usr/lib/postgresql/18/bin

# 升级完成后,统计信息已经在位
# 不需要跑 ANALYZE,查询性能立刻恢复正常

6.2 pg_upgrade 的其他改进

并行检查

# PG 18 新增 --jobs 参数用于并行执行检查步骤
pg_upgrade \
    --jobs 4 \
    --old-datadir /var/lib/postgresql/17/main \
    --new-datadir /var/lib/postgresql/18/main \
    --old-bindir /usr/lib/postgresql/17/bin \
    --new-bindir /usr/lib/postgresql/18/bin

目录交换模式 (--swap)

# --swap 模式:不复制/链接文件,直接交换目录
# 适用于数据目录在不同文件系统的场景
# 升级速度极快,但升级后旧版本数据目录会被重命名
pg_upgrade \
    --swap \
    --old-datadir /var/lib/postgresql/17/main \
    --new-datadir /var/lib/postgresql/18/main \
    --old-bindir /usr/lib/postgresql/17/bin \
    --new-bindir /usr/lib/postgresql/18/bin

对象多时升级更快:当数据库包含大量表、序列等对象时,pg_upgrade 的检查阶段显著加速。

6.3 升级实战:从 PG 17 到 PG 18

#!/bin/bash
# upgrade_pg17_to_18.sh — 生产级升级脚本

set -euo pipefail

# 1. 停止应用写入
systemctl stop myapp

# 2. 记录当前统计信息快照(回滚用)
psql -c "SELECT COUNT(*) FROM pg_statistic" > /tmp/stats_count_before.txt

# 3. 执行 pg_upgrade
/usr/lib/postgresql/18/bin/pg_upgrade \
    --jobs 8 \
    --swap \
    --old-datadir /var/lib/postgresql/17/main \
    --new-datadir /var/lib/postgresql/18/main \
    --old-bindir /usr/lib/postgresql/17/bin \
    --new-bindir /usr/lib/postgresql/18/bin \
    2>&1 | tee /tmp/pg_upgrade.log

# 4. 验证升级
/usr/lib/postgresql/18/bin/pg_isready
psql -c "SELECT version();"
psql -c "SELECT COUNT(*) FROM pg_statistic" > /tmp/stats_count_after.txt

# 5. 配置 AIO
psql -c "ALTER SYSTEM SET io_method = 'worker';"
psql -c "SELECT pg_reload_conf();"

# 6. 启动应用
systemctl start myapp

echo "升级完成!"

七、时态约束(Temporal Constraints):SQL:2023 标准落地

7.1 WITHOUT OVERLAPS:防止时间段重叠

PostgreSQL 18 引入了 SQL:2023 标准中的时态约束,这是一项被企业级应用期待已久的功能。

经典场景:一个员工在同一时间段内只能属于一个部门

-- 创建带时态约束的表
CREATE TABLE employee_departments (
    employee_id INTEGER NOT NULL,
    department_id INTEGER NOT NULL,
    period_start DATE NOT NULL,
    period_end DATE NOT NULL,
    
    -- 时态唯一约束:同一员工的时间段不能重叠
    CONSTRAINT uq_employee_no_overlap 
        UNIQUE (employee_id, period_start, period_end) 
        WITHOUT OVERLAPS
);

-- 插入数据
INSERT INTO employee_departments VALUES 
    (1, 10, '2026-01-01', '2026-06-30');

-- 尝试插入重叠时间段 → 报错!
INSERT INTO employee_departments VALUES 
    (1, 20, '2026-03-01', '2026-09-30');
-- ERROR: conflicting key value violates exclusion constraint "uq_employee_no_overlap"

7.2 PERIOD 子句:时态外键

-- 部门表
CREATE TABLE departments (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    valid_from DATE NOT NULL,
    valid_to DATE NOT NULL,
    PERIOD FOR valid_period (valid_from, valid_to)
);

-- 员工部门关系表带时态外键
CREATE TABLE employee_assignments (
    employee_id INTEGER REFERENCES employees(id),
    department_id INTEGER,
    assigned_from DATE NOT NULL,
    assigned_to DATE NOT NULL,
    PERIOD FOR assign_period (assigned_from, assigned_to),
    
    -- 时态外键:员工分配期间必须在部门有效期内
    CONSTRAINT fk_dept_valid 
        FOREIGN KEY (department_id, PERIOD assign_period) 
        REFERENCES departments (id, PERIOD valid_period)
);

八、NOT ENFORCED 约束:大表加约束的救星

8.1 问题背景

给大表加约束(CHECK、UNIQUE 等)时,PostgreSQL 需要扫描全表验证现有数据,对于 TB 级别的表,这可能需要数小时,期间持有 AccessExclusiveLock,所有读写操作都被阻塞。

8.2 NOT VALID vs NOT ENFORCED

PostgreSQL 早就有 NOT VALID 约束,但 PG 18 新增了 SQL:2023 标准的 NOT ENFORCED

-- NOT VALID:不验证已有数据,但新数据会检查
ALTER TABLE orders ADD CONSTRAINT chk_amount_positive 
    CHECK (amount > 0) NOT VALID;

-- NOT ENFORCED:连新数据也不检查(纯声明性的,用于文档/兼容性)
ALTER TABLE orders ADD CONSTRAINT chk_future_feature 
    CHECK (status IN ('pending', 'processing', 'shipped', 'delivered')) NOT ENFORCED;

两者的区别

特性NOT VALIDNOT ENFORCED
验证已有数据❌ 不验证❌ 不验证
检查新数据✅ 检查❌ 不检查
后续可 VALIDATE✅ 可以
用途加约束不锁表声明性约束,文档/兼容

典型用法:给大表加约束不锁表

-- 第 1 步:不验证地添加约束
ALTER TABLE orders ADD CONSTRAINT chk_amount_positive 
    CHECK (amount > 0) NOT VALID;
-- 瞬间完成,不锁表

-- 第 2 步:后台慢慢验证
-- VALIDATE 不需要 AccessExclusiveLock,只拿 ShareUpdateExclusiveLock
ALTER TABLE orders VALIDATE CONSTRAINT chk_amount_positive;
-- 可能需要较长时间,但不阻塞读写

九、OAuth 2.0 认证:SSO 集成的新篇章

9.1 传统认证的痛点

在企业环境中,PostgreSQL 的认证方式一直是运维的痛点:

  • md5/scram-sha-256:需要为每个用户单独管理密码
  • ldap:依赖 LDAP 服务器,配置复杂
  • certificate:证书管理成本高

PostgreSQL 18 引入了 oauth 认证方法,支持 OAuth 2.0 协议,让你可以直接对接企业 SSO 系统(如 Okta、Azure AD、Keycloak)。

9.2 配置 OAuth 认证

# pg_hba.conf
# TYPE  DATABASE  USER  ADDRESS       METHOD
host    all       all   192.168.1.0/24 oauth

# postgresql.conf
oauth_provider = 'keycloak'
oauth_server_url = 'https://sso.example.com/realms/myrealm'
oauth_client_id = 'postgresql'
oauth_client_secret = 'your-client-secret'
oauth_scope = 'openid profile email'

9.3 连接方式

# 客户端连接时自动跳转 OAuth 流程
psql "host=mydb.example.com dbname=mydb user=john oauth"

# 或者使用环境变量
export PGOAUTH=1
psql "host=mydb.example.com dbname=mydb user=john"

十、MD5 认证正式进入倒计时

PostgreSQL 18 正式废弃了 MD5 密码认证,并明确将在未来版本中移除。

-- 检查你的用户是否还在用 MD5
SELECT rolname, passwd 
FROM pg_authid 
WHERE passwd LIKE 'md5%';

-- 迁移到 SCRAM-SHA-256
ALTER ROLE john PASSWORD 'new-password';
-- 新密码默认用 SCRAM-SHA-256 存储

-- 强制所有新密码使用 SCRAM
-- postgresql.conf
password_encryption = scram-sha-256

迁移脚本

-- 批量重置所有 MD5 用户的密码
-- 注意:需要用户配合设置新密码,这里只是标记
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN SELECT rolname FROM pg_authid WHERE passwd LIKE 'md5%' LOOP
        RAISE NOTICE '用户 % 仍在使用 MD5 认证,请尽快迁移', r.rolname;
    END LOOP;
END $$;

十一、RETURNING 子句增强:OLD 和 NEW 值同时可访问

11.1 旧版的限制

在 PG 18 之前,RETURNING 子句只能返回操作后的新值:

-- PG 17:只能看到更新后的值
UPDATE products SET price = 1999 WHERE id = 1
RETURNING id, price AS new_price;
-- 你看不到更新前的价格

11.2 PG 18 的新语法

-- PG 18:同时获取旧值和新值
UPDATE products SET price = 1999 WHERE id = 1
RETURNING 
    id, 
    OLD.price AS old_price, 
    NEW.price AS new_price,
    NEW.price - OLD.price AS price_diff;

-- 结果:
-- id | old_price | new_price | price_diff
-- 1  | 2999.00   | 1999.00   | -1000.00

-- DELETE 也能用
DELETE FROM archived_orders WHERE order_date < '2025-01-01'
RETURNING 
    id, 
    OLD.order_date AS deleted_order_date,
    OLD.amount AS deleted_amount;

-- MERGE 语句也能用
MERGE INTO products p
USING price_updates u
ON p.id = u.product_id
WHEN MATCHED AND p.price <> u.new_price THEN
    UPDATE SET price = u.new_price
WHEN NOT MATCHED THEN
    INSERT (id, name, price) VALUES (u.product_id, u.name, u.new_price)
RETURNING 
    id,
    OLD.price AS old_price,
    NEW.price AS new_price;

这个特性在审计日志、数据同步、变更追踪等场景下极其有用。


十二、文本处理增强

12.1 PG_UNICODE_FAST 排序规则

PostgreSQL 18 引入了 PG_UNICODE_FAST 排序规则,提供完整的 Unicode 语义,同时加速字符串比较:

-- 使用 PG_UNICODE_FAST
CREATE TABLE multilingual (
    id SERIAL PRIMARY KEY,
    content TEXT COLLATE "PG_UNICODE_FAST"
);

-- 加速 LIKE 查询
SELECT * FROM multilingual 
WHERE content LIKE 'café%' COLLATE "PG_UNICODE_FAST";

12.2 casefold 函数

新增 casefold 函数,提供比 lower 更强大的大小写无关比较:

-- lower 的局限:某些 Unicode 字符的 lower 不完整
SELECT lower('İstanbul');  -- 'i̇stanbul'(带点的 i)

-- casefold:符合 Unicode Case Folding 标准
SELECT casefold('İstanbul');  -- 'istanbul'

-- 实际应用:更可靠的搜索
SELECT * FROM users 
WHERE casefold(name) = casefold('İSTANBUL');

12.3 全文搜索默认排序规则变更

重要破坏性变更:PG 18 的全文搜索现在使用数据库集群的默认排序规则,而不是硬编码的 libc。这意味着升级后可能需要重建全文搜索索引。

-- 升级后检查是否需要重建
SELECT indexrelid::regclass, indclass 
FROM pg_index 
WHERE indclass::regtype::text = 'gin';

-- 重建全文搜索索引
REINDEX INDEX CONCURRENTLY idx_products_search;

十三、安全增强:FIPS 模式和 TLS v1.3

13.1 FIPS 模式验证

PostgreSQL 18 的 pgcrypto 扩展新增 FIPS 模式验证:

-- 检查当前是否运行在 FIPS 模式
SELECT pg_fips_mode();

-- FIPS 模式下只能使用批准的加密算法
-- 以下函数在 FIPS 模式下会报错:
-- encrypt(data, key, 'bf')  ← Blowfish 不被 FIPS 批准
-- encrypt(data, key, 'aes') ← AES 是 FIPS 批准的 ✅

13.2 TLS v1.3 密码套件配置

# postgresql.conf
ssl = on
ssl_min_protocol_version = TLSv1.2
ssl_max_protocol_version = TLSv1.3

# PG 18 新增:精确控制 TLS v1.3 密码套件
ssl_tls13_ciphers = 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'

十四、可观测性增强

14.1 pg_stat_all_tables 新增统计列

-- PG 18:新增 VACUUM 和 ANALYZE 耗时统计
SELECT 
    relname,
    last_vacuum,
    last_autovacuum,
    vacuum_time_ms,        -- 新增:上次 VACUUM 耗时(毫秒)
    autovacuum_time_ms,    -- 新增:上次 autovacuum 耗时
    last_analyze,
    last_autoanalyze,
    analyze_time_ms,       -- 新增:上次 ANALYZE 耗时
    autoanalyze_time_ms    -- 新增:上次 autoanalyze 耗时
FROM pg_stat_all_tables
WHERE schemaname = 'public';

这对于诊断 VACUUM 问题极其有用——你可以一眼看出哪些表的 VACUUM 耗时异常,进而针对性优化。

14.2 pg_backend_memory_contexts 增强

-- 新增 type、path、parent 字段
SELECT 
    name,
    type,       -- 新增:内存上下文类型
    path,       -- 新增:上下文路径
    parent,     -- 新增:父上下文名称
    total_bytes,
    free_bytes,
    used_bytes
FROM pg_backend_memory_contexts
WHERE used_bytes > 1024 * 1024  -- 只看超过 1MB 的
ORDER BY used_bytes DESC;

14.3 pg_stat_checkpointer 改进

-- 新增 num_done 字段
SELECT 
    num_done,           -- 新增:已完成的检查点数量
    num_timed,
    num_requested,
    write_time,
    sync_time
FROM pg_stat_checkpointer;

十五、CREATE FOREIGN TABLE ... LIKE

PostgreSQL 18 新增了基于本地表定义创建外部表的能力:

-- 先有本地表
CREATE TABLE local_orders (
    id BIGSERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    amount NUMERIC(10,2),
    status TEXT DEFAULT 'pending',
    created_at TIMESTAMPTZ DEFAULT now()
);

-- 基于本地表结构创建外部表
CREATE FOREIGN TABLE remote_orders (
    LIKE local_orders
)
SERVER remote_server
OPTIONS (schema_name 'public', table_name 'orders');

-- 现在可以查询远程表了
SELECT * FROM remote_orders WHERE status = 'pending';

这在数据联邦、跨库查询等场景下极大减少了 DDL 维护的工作量——改一处,处处生效。


十六、生产环境升级 Checklist

最后,给你一份经过实战检验的 PG 18 升级检查清单:

## PostgreSQL 18 升级 Checklist

### 升级前
- [ ] 确认所有驱动兼容 PG 18(JDBC、Npgsql、pgx 等)
- [ ] 检查 md5 认证用户,计划迁移到 SCRAM-SHA-256
- [ ] 备份全文搜索和 pg_trgm 索引列表(升级后可能需要重建)
- [ ] 确认 Linux 内核版本,决定 io_method 配置
- [ ] 在测试环境完整演练升级流程
- [ ] 准备回滚方案(pg_upgrade --swap 模式方便回滚)

### 升级中
- [ ] 停止应用写入
- [ ] 执行 pg_upgrade --jobs N --swap
- [ ] 验证统计信息保留
- [ ] 配置 io_method 和 I/O 相关参数

### 升级后
- [ ] 重建全文搜索索引(REINDEX CONCURRENTLY)
- [ ] 配置 AIO 参数并监控 pg_stat_io
- [ ] 验证 Skip Scan 是否生效(EXPLAIN 验证慢查询)
- [ ] 迁移 MD5 用户到 SCRAM
- [ ] 考虑将 UUIDv4 主键迁移到 UUIDv7
- [ ] 更新 pg_hba.conf,配置 OAuth(如需要)
- [ ] 监控 48 小时,关注查询性能和 I/O 指标

总结

PostgreSQL 18 是一个里程碑式的版本。异步 I/O 从根本上重塑了数据库的 I/O 模型,Skip Scan 解决了困扰用户多年的复合索引痛点,UUIDv7 让 UUID 主键不再拖慢写入性能,统计信息跨版本保留让大版本升级不再痛苦。

更值得称道的是,这些改进几乎都是零配置即可受益的——你不需要改一行 SQL,不需要重建表,升级后就能直接享受性能提升。这才是一个成熟数据库该有的样子:让用户无感升级,让系统自动变快

如果你还在犹豫要不要升级,我的建议是:尽快在测试环境验证,然后生产环境跟上。AIO 和 Skip Scan 这两个特性,值得你为它们跑一次升级。


本文基于 PostgreSQL 18 官方发布说明和作者实测编写。部分性能数据因环境差异可能有所不同,建议在自己的硬件上进行基准测试。

推荐文章

批量导入scv数据库
2024-11-17 05:07:51 +0800 CST
PHP 命令行模式后台执行指南
2025-05-14 10:05:31 +0800 CST
nuxt.js服务端渲染框架
2024-11-17 18:20:42 +0800 CST
mysql删除重复数据
2024-11-19 03:19:52 +0800 CST
Grid布局的简洁性和高效性
2024-11-18 03:48:02 +0800 CST
宝塔面板 Nginx 服务管理命令
2024-11-18 17:26:26 +0800 CST
软件定制开发流程
2024-11-19 05:52:28 +0800 CST
程序员茄子在线接单