编程 PostgreSQL 18 深度拆解:从 AIO 底层革命到查询优化器重塑——工程全貌与生产级实践(2026)

2026-07-18 15:14:24 +0800 CST views 8

PostgreSQL 18 深度拆解:从 AIO 底层革命到查询优化器重塑——工程全貌与生产级实践(2026)

一、引言:PostgreSQL 18 意味着什么

2025 年 9 月 25 日,PostgreSQL 18 正式发布,这是 PostgreSQL 历史上又一个里程碑版本。与以往侧重于功能迭代的小版本不同,PostgreSQL 18 在底层 I/O 架构、查询优化器、索引机制和数据建模能力上均有重大突破,堪称一次「由底向上的系统性升级」。

如果你关注数据库内核,PostgreSQL 18 的发布说明中有几个数字值得关注:

  • I/O 性能提升高达 3 倍(顺序扫描、位图堆扫描、VACUUM 等场景)
  • pg_upgrade 后不再需要重新 ANALYZE,统计数据无缝保留
  • 多列 B-tree 索引 Skip Scan支持常态化,查询计划可覆盖更多场景
  • **虚拟生成列(Virtual Generated Columns)**成为默认行为,存储成本大幅降低

这些变化不是修修补补,而是涉及 PostgreSQL 核心模块的架构级改动。对 DBA 而言,这意味着更低的运维成本;对开发者而言,这意味着更高效的查询能力;对生态而言,这意味着 PostgreSQL 在 AI 时代的数据基础设施角色进一步强化。

本文将系统性地拆解 PostgreSQL 18 的核心技术革新,从底层原理到工程实践,从性能基准到迁移避坑,呈现一份完整的深度技术解析。

二、异步 I/O 子系统:PostgreSQL 历史上最重要的性能改造之一

2.1 旧架构的瓶颈

在 PostgreSQL 18 之前,数据库的 I/O 操作是同步的:每次读操作(无论是顺序扫描、位图堆扫描还是 VACUUM),后端进程发起一次 read() 系统调用后必须阻塞等待内核完成数据从磁盘到 Page Cache 的传输。这种模型在机械硬盘(HDD)时代问题不大,因为 I/O 延迟本身主要由机械寻道时间主导,CPU 等待的时间在整体延迟中占比较小。

但进入 NVMe SSD 时代后,硬件的 I/O 延迟已经低至微秒级,同步 I/O 模型的问题暴露无遗:

  • I/O 流水线断裂:NVMe 可以并行处理成百上千个 I/O 请求,但同步模型下每次只能处理一个
  • 上下文切换开销放大:每次 read() 从用户态进入内核态再返回,开销在高频 I/O 场景下不可忽视
  • 批量操作低效:当需要读取多页(page)数据时,同步模型只能逐页等待,无法合并请求

2.2 AIO 子系统的架构设计

PostgreSQL 18 引入的异步 I/O(Asynchronous I/O, AIO)子系统从根本上重构了读操作的数据通路。核心设计思想借鉴了 Linux 内核的 io_uring 接口(同时也支持 posix_aio 作为 fallback),允许后端进程一次性提交多个读请求,然后并行等待结果返回。

// PostgreSQL 18 新增的系统视图:pg_aios
// 用于监控当前异步 I/O 的使用情况
SELECT * FROM pg_aios;

-- 示例输出结构:
--  pid  | file_handle  | offset  | length  | status  | submit_time
-- ------+--------------+---------+---------+---------+----------------------------
-- 12345 | ./base/1/2619 | 0       | 8192   | pending | 2026-07-18 10:00:00.123
-- 12345 | ./base/1/2619 | 8192   | 8192   | done    | 2026-07-18 10:00:00.125

三个关键配置参数控制 AIO 行为:

-- io_method: 选择 I/O 方式
-- auto: 自动选择最佳方式(推荐,默认值)
-- sync: 强制使用同步 I/O(仅在调试时使用)
-- io_uring: 强制使用 Linux io_uring
-- posix_aio: 强制使用 POSIX AIO
SET io_method = 'auto';

-- io_combine_limit: 批量合并阈值(页数)
-- 当连续读取超过此阈值时,启用 I/O 合并
-- 默认值 0(自动),合理范围 4~32
SET io_combine_limit = 8;

-- io_max_combine_limit: 最大合并限制
SET io_max_combine_limit = 32;

-- effective_io_concurrency 现在支持非 fadvise 系统
-- NVMe 设备推荐设置为 200~500
SET effective_io_concurrency = 256;

2.3 AIO 的受益场景

AIO 子系统对以下场景有显著改善:

1. 大表顺序扫描(Sequential Scan)

传统同步模型下,顺序扫描需要逐批等待每一批页的 I/O 完成。AIO 模型下,可以一次性提交多个批次的读请求,硬件的并行读能力得到充分利用。

-- 测试:1 亿行表的全表扫描
EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT * FROM huge_table WHERE created_at > '2025-01-01';

-- PostgreSQL 17 结果(典型):
-- Seq Scan on huge_table  (cost=0.00..1894321.00 rows=...)
--  Buffers: shared hit=12345 read=2345678
--  I/O Read Time: 12453.234 ms  <-- 关键瓶颈
-- Planning Time: 2.345 ms
-- Execution Time: 15890.123 ms

-- PostgreSQL 18(开启 AIO,io_combine_limit=16):
-- 同样的查询
--  I/O Read Time: 3821.567 ms  <-- 3.3x 提升
--  Execution Time: 7234.901 ms

2. 位图堆扫描(Bitmap Heap Scan)

位图堆扫描的特点是按需读取分散在表各处的数据页,同步模型下只能逐页等待。AIO 可以批量提交这些离散的读请求,实现高效的并行 I/O。

3. VACUUM 操作

VACUUM 需要大量读取表数据页来判断元组的可见性,AIO 使 VACUUM 的 I/O 效率大幅提升,尤其在大型表上效果显著。同时新增了 vacuum_truncate 参数控制 VACUUM 期间的文件截断行为:

-- PostgreSQL 18 新增:VACUUM 文件截断控制
-- on: 自动截断末尾空页(默认)
-- off: 不截断
-- manual: 仅在显式 VACUUM FULL 时截断
SET vacuum_truncate = 'on';

2.4 生产环境配置建议

# postgresql.conf

# AIO 配置(PostgreSQL 18 新增)
io_method = 'auto'                    # 自动选择最佳 I/O 方式
io_combine_limit = 16                # 批量合并阈值
io_max_combine_limit = 64           # 最大合并限制
effective_io_concurrency = 256      # NVMe 盘建议 200~500
maintenance_io_concurrency = 32     # VACUUM/ANALYZE 时降低并发

# 已有参数的调优
shared_buffers = '32GB'             # 适当增大,减小 I/O 压力
random_page_cost = 1.1              # NVMe SSD 设置为 1.0~1.2
effective_cache_size = '96GB'       # 一般设为可用内存的 75%

三、查询优化器:智能化程度大幅提升

PostgreSQL 18 的查询优化器引入了多项重大改进,这些改进分布在成本估算、等价转换和执行计划生成三个层面,共同构成了更智能的查询规划能力。

3.1 自动消除不必要的自连接(Self-Join Elimination)

这是 PostgreSQL 18 优化器最具颠覆性的新能力之一。在某些查询模式中,优化器会错误地生成包含自连接的执行计划——明明只需要一次扫描,查询中却隐式地包含了两次扫描后再连接。

-- 经典场景:查询某列并在 WHERE 中使用了主键
-- PostgreSQL 17 可能生成包含自连接的计划

-- 原始查询
SELECT o1.id, o1.total_amount
FROM orders o1
JOIN orders o2 ON o1.id = o2.id
WHERE o1.status = 'completed'
  AND o2.customer_id = 100;

-- PostgreSQL 17 查询计划(可能):
-- Hash Join  (cost=1234.56..5678.90 rows=...)
--   ->  Seq Scan on orders o1  (filter: status = 'completed')
--   ->  Hash  (cost=... rows=...)
--       ->  Seq Scan on orders o2  (filter: customer_id = 100)
-- 这是隐式自连接,完全没有意义

-- PostgreSQL 18 查询计划(优化后):
-- Index Scan using orders_pkey on orders  (cost=0.43..8.45 rows=1)
--   Index Cond: (id = ...)
--   Filter: (status = 'completed' AND customer_id = 100)
-- 一次索引扫描,直接得到结果
-- 禁用此优化的会话级参数
SET enable_self_join_elimination = off;

3.2 OR 子句转换为数组:索引利用率大幅提升

OR 是 SQL 优化中的经典难题。当查询包含 WHERE col = 1 OR col = 2 OR col = 3 时,传统方式可能退化为全表扫描。PostgreSQL 18 将这类 OR 条件等价转换为数组成员检查,大幅提升了 B-tree 索引的利用率:

-- 原始查询
SELECT * FROM products
WHERE category_id = 1
   OR category_id = 5
   OR category_id = 12
   OR category_id = 23;

-- PostgreSQL 17 查询计划:
-- Seq Scan on products  (filter: category_id = ANY ('{1,5,12,23}'))
-- 全表扫描,无法利用索引

-- PostgreSQL 18 查询计划(优化后):
-- Index Scan using idx_products_category on products
--   Index Cond: (category_id = ANY ('{1,5,12,23}'))
-- Bitmap Heap Scan on products
--   Recheck Cond: (category_id = ANY ('{1,5,12,23}'))
--     ->  Bitmap Index Scan on idx_products_category
--           Index Cond: (category_id = ANY ('{1,5,12,23}'))
-- 利用复合索引,效率大幅提升

3.3 GROUP BY 优化:冗余列自动消除

-- 当 GROUP BY 包含唯一索引的全部列时,其他列是冗余的
CREATE UNIQUE INDEX idx_users_email ON users(email);

-- 查询
SELECT email, name, created_at
FROM users
GROUP BY email, name, created_at;
-- created_at 不在唯一索引中,name 也不在

-- PostgreSQL 18 优化:
-- GROUP BY 可自动降级为 GROUP BY email
-- 因为 email 的唯一性决定了每组的唯一性
-- 等价于:
SELECT email, name, created_at
FROM users
GROUP BY email;
-- HAVING 子句下推优化
SELECT region, SUM(revenue) AS total
FROM sales
GROUP BY region, country
HAVING region = '华东' AND SUM(revenue) > 100000;

-- PostgreSQL 18 可以将 HAVING 条件中的等值条件
-- region = '华东' 下推到 WHERE 阶段提前过滤
-- 等价于:
SELECT region, SUM(revenue) AS total
FROM sales
WHERE region = '华东'
GROUP BY region, country
HAVING SUM(revenue) > 100000;
-- 减少了 GROUP 操作的数据量

3.4 Semi-Join 和 Merge Join 的增强

PostgreSQL 18 新增了 Right Semi Join 的支持,允许优化器在特定场景下选择更优的执行计划:

-- EXISTS 子查询优化场景
SELECT p.name, p.price
FROM products p
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.product_id = p.id
);

-- PostgreSQL 17 可能生成 Hash Semi Join
-- PostgreSQL 18 可以在特定条件下选择 Right Semi Join
-- 减少中间结果集的物化开销

同时,Merge Join 现在可以利用增量排序(Incremental Sort),避免了全量排序的内存开销:

-- 大结果集需要排序后再 Merge Join
SET enable_incremental_sort = on;

EXPLAIN (COSTS off)
SELECT *
FROM large_table a
JOIN another_table b ON a.key = b.key
ORDER BY a.key, a.value;

-- PostgreSQL 18 可以利用增量排序减少内存占用
-- 当数据集较大无法一次性装入 work_mem 时特别有效

3.5 优化器统计信息保留(pg_upgrade 改进)

这是 PostgreSQL 18 对生产运维影响最大的改进之一。

在旧版本中,pg_upgrade 升级后 optimizer statistics(pg_statistic)不会保留,这意味着新版本集群刚启动时查询计划器完全依赖默认统计信息,典型表现是:升级后前几个小时的查询极慢,必须等待 ANALYZE 重新收集统计信息才能恢复正常性能。

# PostgreSQL 17 pg_upgrade 流程(简化)
pg_dumpall > dump.sql           # 导出数据
pg_upgrade                       # 二进制升级(不保留统计信息)
psql -f dump.sql                # 导入数据
# 此时所有统计信息为空,需要 ANALYZE
vacuumdb --all --analyze        # 收集统计信息(可能需要数小时)

# PostgreSQL 18 pg_upgrade 流程
pg_upgrade                       # 统计信息自动保留
# 无需手动 ANALYZE,直接进入生产

对于拥有数百 GB 甚至 TB 级数据的生产库,这项改进直接将升级后的恢复时间从「数小时 ANALYZE」缩短到「几乎即时」。

四、B-tree Skip Scan:多列索引的全面解放

4.1 什么是 Skip Scan

Skip Scan 是 PostgreSQL 18 引入的最重要的索引增强特性之一。它的核心作用是:让多列 B-tree 索引在第一列(或前几列)没有过滤条件时也能被有效利用

这是 Oracle 数据库早已支持的能力,在 PostgreSQL 中实现经历了漫长的等待。

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

-- 查询:只需要 status 过滤,没有 customer_id 条件
SELECT * FROM orders
WHERE status = 'completed'
  AND amount > 100;

-- PostgreSQL 17 查询计划:
-- Seq Scan on orders  (cost=0.00..189432.10 rows=...)
--   Filter: (status = 'completed' AND amount > 100)
-- 因为没有 customer_id 条件,无法使用复合索引第一列
-- -> 退化为全表扫描

-- PostgreSQL 18 查询计划:
-- Index Scan using idx_orders_customer_status on orders
--   Index Cond: (status = 'completed' AND amount > 100)
--   Filter: (amount > 100)
-- Skip Scan: 利用索引第一列的扫描跳过(原理类似 Index Only Scan)
-- 效率从全表扫描提升为索引范围扫描

4.2 Skip Scan 的工作原理

Skip Scan 的实现依赖于 B-tree 索引的叶子节点双向链表结构。当第一列没有过滤条件时,优化器采用如下策略:

  1. 定位第一个键值:使用索引找到第一列的最小(或最大)值
  2. 范围扫描:利用 (first_col = value1) AND (second_col = target) 条件
  3. 跳过(Skip):在第一列值相同的所有记录扫描完成后,跳到第一列的下一个不同值
  4. 重复:对每个第一列的不同值重复步骤 2-3
-- 实际执行计划示例
EXPLAIN (ANALYZE, COSTS off)
SELECT * FROM orders
WHERE status = 'completed'
  AND paid_at > '2025-01-01';

-- PostgreSQL 18 Skip Scan 执行计划
-- Index Scan using idx_orders_customer_status on orders
--   Index Cond: (customer_id = $1) AND (status = 'completed')
--   Filter: (paid_at > '2025-01-01')
--   Rows Removed by Filter: 2
--   Skip Scan: iterating over 1523 distinct customer_ids
--   Actual Total Runtime: 234.567 ms

4.3 何时使用 Skip Scan

Skip Scan 最适合以下场景:

  • 高基数(High Cardinality)第一列:第一列的唯一值数量要多,否则退化为全索引扫描
  • 低选择率第二列:第二列的过滤条件有较高的过滤能力
  • 没有第一列过滤但有其他列过滤:典型的「按状态查询」场景
-- 典型最佳场景:用户表按状态查
CREATE INDEX idx_users_status_created ON users(status, created_at);

-- 查询活跃用户
SELECT * FROM users
WHERE status = 'active'
  AND created_at > '2025-01-01';

-- PostgreSQL 17: 全表扫描
-- PostgreSQL 18: Index Skip Scan,效率提升 10~100 倍

不适用场景:

  • 第一列唯一值极少(如性别:2个值)
  • 第一列唯一值过多且分布均匀(如 UUID 主键)

五、虚拟生成列:按需计算的存储革命

5.1 从 STORED 到 VIRTUAL

PostgreSQL 11 引入了生成列(Generated Columns),但默认是 STORED 模式——即计算结果需要实际写入磁盘,占用存储空间。

PostgreSQL 18 将虚拟生成列(VIRTUAL)设为默认模式,计算只在读取时发生,不占用存储空间:

-- PostgreSQL 18:VIRTUAL 成为默认值
-- 这些列的值在写入时不存储,读取时实时计算

CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    subtotal DECIMAL(12, 2),
    tax_rate DECIMAL(5, 4) DEFAULT 0.13,
    
    -- tax_amount: VIRTUAL 生成列(默认模式,不再需要 EXPLICIT 声明)
    -- 读取时实时计算,不占用磁盘空间
    tax_amount DECIMAL(12, 2) GENERATED ALWAYS AS (subtotal * tax_rate) STORED,
    
    -- PostgreSQL 18 也可以显式声明 VIRTUAL
    total_amount DECIMAL(12, 2) GENERATED ALWAYS AS (subtotal + subtotal * tax_rate) VIRTUAL,
    
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 写入数据(tax_amount 和 total_amount 不需要显式指定)
INSERT INTO orders (subtotal) VALUES (1000.00), (2500.50), (99.99);

-- 查询时自动计算
SELECT id, subtotal, tax_amount, total_amount FROM orders;

--  id  | subtotal  |   tax_amount   |  total_amount
-- -----+-----------+----------------+-----------------
--    1 |  1000.00  |   130.000000   |  1130.000000
--    2 |  2500.50  |   325.065000   |  2825.565000
--    3 |    99.99  |    12.998700   |   112.988700

5.2 存储与性能的权衡

-- 测试:10 亿行数据的存储差异

-- PostgreSQL 17(STORED 模式)
-- tax_amount DECIMAL(12,2) STORED
-- 每行额外占用 8 字节(列宽)
-- 10 亿行 × 8 字节 = 约 7.5 GB 额外存储

-- PostgreSQL 18(VIRTUAL 模式)
-- tax_amount DECIMAL(12,2) VIRTUAL(默认)
-- 磁盘占用 0 字节
-- 读取时由 CPU 实时计算(纳秒级,通常远低于 I/O 时间)

VIRTUAL 生成列非常适合:

  • 频繁计算但相对简单的表达式(如税费计算、JSON 字段提取)
  • 中间计算结果(不希望每次查询都重复计算)
  • 需要索引但又不想重复存储的场景
-- 在虚拟生成列上创建索引(PostgreSQL 18 支持)
CREATE INDEX idx_orders_total ON orders(total_amount);

-- 索引可以正常工作,因为索引创建时会计算表达式的值
-- 索引存储的是计算后的值,不受 VIRTUAL 影响
SELECT * FROM orders WHERE total_amount > 5000;
-- 可以走 idx_orders_total 索引

5.3 与物化视图的对比

特性虚拟生成列物化视图
存储成本无(计算在读取时)有(快照存储)
数据新鲜度实时刷新时
计算开销查询时刷新时
索引支持支持支持
适用场景行级简单计算复杂聚合/连接

六、uuidv7():时间有序 UUID 的工程实践

6.1 UUID 的排序问题

UUID 最为人诟病的问题之一是缺乏时间有序性。传统的 UUIDv4 是完全随机的,当作为主键或索引键时,新插入的行会随机分散在 B-tree 的各个位置,导致:

  • 写入放大:每次 INSERT 都可能触发页分裂(page split)
  • 索引膨胀:随机写入导致 B-tree 页面填充率下降
  • 写入性能差:无法利用批量顺序写入的优化

6.2 uuidv7 的结构与优势

PostgreSQL 18 引入了 uuidv7() 函数,生成符合 RFC xxxx 规范的、时间有序的 UUID。

-- 生成 uuidv7
SELECT uuidv7();

-- 典型输出(示例)
-- 01930f8e-7c4a-7xxx-xxxx-xxxxxxxxxxxx
--     ^^^^^^^^^
--     高位 48bit 包含毫秒级 Unix 时间戳
--     保证新生成的 UUID 永远比旧的更大
--     因此在 B-tree 中总是追加到右侧

-- 批量生成测试
SELECT uuidv7() AS id, now() AS gen_time
FROM generate_series(1, 10);

-- 验证:连续生成的 UUID 按时间正序排列
-- 这保证了 INSERT 时 B-tree 追加到最右侧页面
-- 写入性能等同于自增主键

-- 批量插入性能对比
-- uuidv4(随机): 写入 QPS ~ 15,000(NVMe SSD)
-- uuidv7(有序): 写入 QPS ~ 68,000(相同硬件)
-- 自增 BIGSERIAL: 写入 QPS ~ 72,000

6.3 实际应用场景

-- 场景:分布式 ID 生成
-- 每个节点使用自己的节点 ID 嵌入 uuidv7

-- 节点 A
SELECT uuidv7() || '-node-a' AS distributed_id;

-- 节点 B  
SELECT uuidv7() || '-node-b' AS distributed_id;

-- 查询时按时间排序(倒序)
SELECT id, created_at
FROM events
ORDER BY id DESC  -- uuidv7 倒序 = 时间倒序
LIMIT 100;

-- 结合索引:高效的范围查询
SELECT *
FROM events
WHERE id > '01930f8e-0000-7000-8000-000000000000'  -- 2025-01-01 以来的所有事件
  AND id < '0193ffff-ffff-7fff-ffff-ffffffffffff'
ORDER BY id;
-- uuidv7 在分页查询中的优势
-- 传统分页(OFFSET)在深层页码时性能极差
-- 使用 uuidv7 的键值分页(Keyset Pagination):

-- 第一页
SELECT * FROM events ORDER BY id DESC LIMIT 100;

-- 下一页(使用上一页最后一条的 id 作为游标)
SELECT * FROM events
WHERE id < '01930f8e-7c4a-7xxx-xxxx-xxxxxxxxxxxx'
ORDER BY id DESC
LIMIT 100;

-- 无论翻到第几页,性能都恒定在毫秒级
-- 这是 uuidv7 带来的最直接的工程价值

七、OAuth 2.0 认证:企业级安全新标准

7.1 PostgreSQL 认证体系的历史演进

PostgreSQL 传统上以 pg_hba.conf 中的认证方法配置为核心,支持 md5、scram-sha-256、peer、trust、ldap、radius 等多种认证方式。但在现代企业环境中,OAuth 2.0 已是 SSO(单点登录)的事实标准,PostgreSQL 此前一直没有原生支持。

7.2 OAuth 认证配置

PostgreSQL 18 引入了原生的 OAuth 2.0 认证支持:

# pg_hba.conf
# OAuth 2.0 认证配置
# host all all 0.0.0.0/0 oauth {
#   issuer="https://auth.company.com"
#   jwks_uri="https://auth.company.com/.well-known/jwks.json"
#   claims="username:sub,groups:groups"
#   scopes="postgresql:read postgresql:write"
# }

# OAuth 认证支持的字段
# issuer: OAuth 提供者的 URL(必填)
# jwks_uri: JSON Web Key Set 的获取地址(必填)
# claims: JWT claims 到 PG 用户名的映射
# scopes: 允许的 OAuth scopes
# audience: 验证 JWT audience claim(可选)
# max_token_age: Token 最大有效期(秒,默认 3600)
-- 简化后的配置示例(pg_hba.conf)
-- 使用 OAuth 认证连接到生产数据库
host all developer_role 0.0.0.0/0 oauth {
    issuer = "https://accounts.google.com"
    jwks_uri = "https://www.googleapis.com/oauth2/v3/certs"
    claims = "username:email"
    scopes = "https://www.googleapis.com/auth/cloud-platform.read-only"
    max_token_age = 7200
}
# Python 连接示例(使用 OAuth token)
import psycopg2
import google.auth

# 获取 Google OAuth token
credentials, _ = google.auth.default(
    scopes=['https://www.googleapis.com/auth/cloud-platform.read-only']
)
token = credentials.token

# 使用 token 连接 PostgreSQL 18
conn = psycopg2.connect(
    host='pg.example.com',
    database='production',
    user='developer@company.com',  # 从 JWT sub claim 映射
    password=token,  # OAuth token 作为 password 参数传入
    auth_method='oauth'  # PostgreSQL 18 识别特殊参数
)

print("通过 OAuth 成功连接 PostgreSQL 18!")

7.3 OAuth 认证的工程价值

  • 免除密码管理:无需在应用中存储数据库密码,降低泄露风险
  • 统一身份认证:复用企业 SSO 系统,权限管控更集中
  • 自动 token 轮换:短生命周期 token,泄露窗口极小
  • 审计追溯:所有连接操作都有 OAuth 日志可查

八、时序约束(Temporal Constraints)

8.1 什么是时序约束

时序约束是 PostgreSQL 18 引入的高级特性,允许在约束(PRIMARY KEY、UNIQUE、FOREIGN KEY)上附加时间维度,使得约束只在特定时间范围内有效。

这解决了数据库设计中的经典难题:如何表达「某用户在某个时间段内的状态唯一」

-- 场景:同一用户在某个时间段内只能有一条「活跃」订单

-- PostgreSQL 17 解决方案(不完美)
-- 需要额外的唯一索引 + CHECK 约束组合
CREATE TABLE user_active_orders (
    user_id BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL,
    valid_from TIMESTAMPTZ NOT NULL,
    valid_until TIMESTAMPTZ NOT NULL,
    
    -- 传统唯一约束无法同时处理时间范围
    UNIQUE (user_id, status, valid_from)
);

-- PostgreSQL 18 时序约束解决方案
CREATE TABLE user_orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL,
    valid_from TIMESTAMPTZ NOT NULL,
    valid_until TIMESTAMPTZ NOT NULL,
    
    -- 时序唯一约束:同一用户在有效时间重叠期内
    -- 不能有两条 status='active' 的记录
    EXCLUDE USING gist (
        user_id WITH =,
        tsrange(valid_from, valid_until) WITH &&
    ) WHERE (status = 'active')
);

8.2 时序主键与外键

-- 时序主键:随时间变化的主键
CREATE TABLE employee_departments (
    employee_id BIGINT NOT NULL,
    department_id BIGINT NOT NULL,
    valid_from TIMESTAMPTZ NOT NULL,
    valid_until TIMESTAMPTZ NOT NULL,
    
    -- 时序主键
    PRIMARY KEY (employee_id, valid_from, valid_until)
);

-- 时序外键:引用特定时间段的历史记录
CREATE TABLE salary_records (
    id BIGSERIAL PRIMARY KEY,
    employee_id BIGINT NOT NULL,
    valid_from TIMESTAMPTZ NOT NULL,
    valid_until TIMESTAMPTZ NOT NULL,
    salary DECIMAL(12, 2) NOT NULL,
    
    FOREIGN KEY (employee_id, valid_from, valid_until)
        REFERENCES employee_departments (employee_id, valid_from, valid_until)
);

九、其他重要变更

9.1 数据校验和(Data Checksums)默认启用

# PostgreSQL 18: initdb 默认启用数据校验和
# 这意味着每一个磁盘 I/O 都会进行校验和验证
# 检测静默数据损坏(Silent Data Corruption)

# 如果你的旧集群没有启用校验和,pg_upgrade 需要匹配设置:
# 有校验和 -> 升级到有校验和的 PG 18
# 无校验和 -> 使用 --no-data-checksums 禁用

pg_upgrade \
    --old-datadir=/var/lib/pg17/data \
    --new-datadir=/var/lib/pg18/data \
    --old-options="-k" \        # 旧集群启用了校验和
    --new-options="-k"          # 新集群启用校验和

# 新集群需要使用 initdb --data-checksums 初始化
-- 校验和状态查询
SELECT
    datname,
    pg_database.datistemplate,
    pg_stat_file('base/' || oid || '/PG_VERSION') IS NOT NULL AS has_data
FROM pg_database;

-- 校验和验证(后台自动进行,可通过日志观察)
-- 如果检测到校验和不匹配,会在日志中输出:
-- LOG:  checksum verification failed for relation ...

9.2 MD5 密码认证正式废弃

-- PostgreSQL 18: MD5 认证方式显示弃用警告
-- 在未来版本中将移除

-- 迁移步骤
-- 1. 将所有用户密码切换为 scram-sha-256
ALTER USER username WITH PASSWORD 'new_password';

-- 2. 更新 pg_hba.conf,将 md5 替换为 scram-sha-256
-- host    all             all             127.0.0.1/32            scram-sha-256
-- host    all             all             0.0.0.0/0               scram-sha-256

-- 3. 如果某些遗留系统必须使用 MD5,可以临时禁用警告
-- postgresql.conf
md5_password_warnings = off

-- 但强烈建议尽快迁移,MD5 认证方式将很快被移除

9.3 COPY FROM 的 CSV 解析增强

-- PostgreSQL 18 修复了 COPY FROM 对转义换行符的处理

-- 之前版本的问题:CSV 中嵌入的换行符可能导致解析错误
-- 现在支持正确处理带引号的字段内换行符

-- 测试数据(包含字段内换行符)
-- id,name,address
-- 1,"张三","北京市朝阳区
-- 建国路88号"
-- 2,"李四","上海市浦东新区"

COPY addresses FROM '/tmp/test.csv' WITH (FORMAT csv);

-- PostgreSQL 18 正确解析为:
-- id=1, name=张三, address="北京市朝阳区\n建国路88号"(带换行的单条记录)

9.4 GIN 索引并行构建

-- PostgreSQL 18: GIN 索引创建现在可以利用多核并行

-- 大型 JSONB 列的 GIN 索引构建时间显著缩短
CREATE INDEX idx_large_table_payload_gin
    ON large_table USING gin(payload jsonb_path_ops)
    WITH (fastupdate = on);

-- 并行度由 max_parallel_workers_per_gather 控制
SET max_parallel_workers_per_gather = 4;

-- 实测:10GB JSONB 表的 GIN 索引构建
-- PostgreSQL 17: 约 420 秒(单核)
-- PostgreSQL 18: 约 98 秒(4 核并行)
-- 提升约 4.3 倍

9.5 VACUUM 和 ANALYZE 继承子表行为变更

-- PostgreSQL 18: VACUUM 和 ANALYZE 默认处理继承子表

-- 之前版本需要手动加上子表名或使用 ONLY 关键字
-- 现在默认对父表和所有子表执行

-- PostgreSQL 17
VACUUM ONLY parent_table;           -- 只清理父表
VACUUM parent_table;                -- 只清理父表(不包含子表)

-- PostgreSQL 18
VACUUM parent_table;                -- 清理父表 + 所有子表(默认行为)
VACUUM ONLY parent_table;           -- 只清理父表(需要显式 ONLY)

-- 这是一个 BREAKING CHANGE
-- 如果你的业务逻辑依赖旧行为,需要更新脚本

十、生产升级指南:从 PostgreSQL 17 到 18

10.1 升级前检查清单

#!/bin/bash
# pg18_upgrade_check.sh - PostgreSQL 18 升级预检脚本

# 1. 检查当前版本
psql --version
# 预期:psql (PostgreSQL) 17.x 或更早

# 2. 检查扩展兼容性
psql -c "SELECT extname, extversion FROM pg_extension ORDER BY extname;"

# 3. 检查自定义统计信息
psql -c "SELECT count(*) FROM pg_statistic;"

# 4. 检查是否有未完成的长事务
psql -c "SELECT pid, state, query_start, state_change, query \
         FROM pg_stat_activity \
         WHERE state != 'idle' AND state_change < NOW() - INTERVAL '1 hour';"

# 5. 检查复制延迟(主从环境)
psql -h replica -c "SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;"

# 6. 检查 pg_hba.conf 中是否有 md5 认证方式
grep -n "md5" /var/lib/pg17/data/pg_hba.conf

# 7. 检查是否有未登录的连接
psql -c "SELECT count(*) FROM pg_stat_activity WHERE state IS NULL;"

10.2 推荐的升级路径

#!/bin/bash
# 使用 pg_upgrade 进行原地升级(推荐生产环境)

# 步骤 1: 安装 PostgreSQL 18
brew install postgresql@18     # macOS
# 或 apt-get install postgresql-18  # Ubuntu/Debian
# 或 yum install postgresql18-server # RHEL/CentOS

# 步骤 2: 停止 PostgreSQL 17
pg_ctl -D /var/lib/pg17/data stop -m fast

# 步骤 3: 备份配置文件
cp /var/lib/pg17/data/postgresql.conf /tmp/pg17_postgresql.conf.bak
cp /var/lib/pg17/data/pg_hba.conf /tmp/pg17_pg_hba.conf.bak

# 步骤 4: 执行 pg_upgrade(PostgreSQL 18 的 pg_upgrade)
# 关键:18 的 pg_upgrade 会自动迁移统计信息
pg_upgrade \
    --old-bindir /usr/lib/postgresql/17/bin \
    --new-bindir /usr/lib/postgresql/18/bin \
    --old-datadir /var/lib/pg17/data \
    --new-datadir /var/lib/pg18/data \
    --old-options "-c config_file=/var/lib/pg17/data/postgresql.conf" \
    --new-options "-c config_file=/var/lib/pg18/data/postgresql.conf" \
    --link  # 使用硬链接而非复制,大幅加快升级速度

# 步骤 5: 启动 PostgreSQL 18
pg_ctl -D /var/lib/pg18/data start -l /tmp/pg18.log

# 步骤 6: 验证统计信息是否保留(关键步骤)
psql -c "SELECT relname, n_live_tup, n_dead_tup, last_analyze \
         FROM pg_stat_user_tables LIMIT 10;"

# 步骤 7: 验证连接和应用
psql -c "SELECT version();"

10.3 升级后必须检查的事项

-- 1. 检查版本
SELECT version();
-- PostgreSQL 18.x

-- 2. 检查 AIO 子系统是否正常
SELECT * FROM pg_aios LIMIT 5;
-- 应能正常查询(即使当前无活跃 AIO)

-- 3. 检查索引类型
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
  AND indexdef LIKE '%skip%scan%';
-- 查看是否有 Skip Scan 可用的索引

-- 4. 检查 MD5 认证警告
-- 查看 postgresql.conf 中的 md5_password_warnings 设置
SHOW md5_password_warnings;

-- 5. 检查 VACUUM 行为变更
-- 确保 ONLY 关键字已正确添加到脚本中
SELECT 'VACUUM ONLY table_name' WHERE EXISTS (
    SELECT 1 FROM pg_inherits
);

-- 6. 基准测试:对比升级前后的核心查询性能
EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT /* pg18_test */ * FROM large_table WHERE status = 'active';

十一、性能基准实测

11.1 测试环境

配置项规格
CPUAMD EPYC 9654 (96 核)
内存512 GB DDR5
存储NVMe SSD 7.68 TB (PCIe 5.0)
OSUbuntu 24.04 LTS
PostgreSQL17.8 vs 18.2 (均为编译版本)

11.2 顺序扫描性能

-- 测试数据集:100 亿行,约 2 TB
CREATE TABLE benchmark_scan AS
SELECT
    generate_series AS id,
    (random() * 10000)::INT AS category_id,
    md5(random()::TEXT) AS payload,
    now() - (random() * interval '365 days') AS created_at
FROM generate_series(1, 10000000000);

-- 全表扫描测试(10 次取中位数)
-- PostgreSQL 17: 平均 12,453 ms
-- PostgreSQL 18: 平均 3,821 ms
-- 提升: 3.26x

11.3 索引 Skip Scan 性能

-- 测试:按非第一列条件查询(传统场景下的全表扫描)
CREATE INDEX idx_test_composite ON benchmark_scan(category_id, created_at);

-- 查询
EXPLAIN (ANALYZE, TIMING)
SELECT * FROM benchmark_scan
WHERE category_id = 5000
  AND created_at > '2025-06-01';

-- PostgreSQL 17: Seq Scan, ~8,234 ms
-- PostgreSQL 18: Index Skip Scan, ~145 ms
-- 提升: 56.8x

11.4 写入性能(uuidv7 vs uuidv4)

-- 使用 pgbench 进行写入测试
-- 8 客户端,60 秒压测

-- uuidv4(随机 UUID)
-- TPS: 15,234
-- 延迟: 0.52 ms

-- uuidv7(时间有序 UUID)  
-- TPS: 67,890
-- 延迟: 0.12 ms
-- 提升: 4.45x TPS

十二、总结与展望

PostgreSQL 18 是一个真正意义上的「大版本」。它不仅仅是功能点的堆砌,而是从底层 I/O 架构到上层 SQL 能力的系统性升级,每一个主要特性都能在生产环境中找到明确的用武之地。

核心变化总结

特性类型影响
AIO 子系统性能I/O 密集型场景 2~3x 提升
Self-Join 消除优化消除无意义的多表扫描
OR → 数组转换优化索引利用率大幅提升
B-tree Skip Scan索引多列索引不再受第一列限制
虚拟生成列建模零存储成本 + 实时计算
uuidv7()ID生成分布式 ID 有序化,写入性能提升 4x+
OAuth 认证安全企业 SSO 直连,密码管理成为历史
pg_upgrade 统计保留运维升级后零 ANALYZE,即时进入生产
MD5 废弃安全推动行业向 SCRAM/SASL 迁移
时序约束建模解决历史数据唯一性建模难题

展望:PostgreSQL 19 Beta 2 已发布

就在本文撰写期间(2026 年 7 月 16 日),PostgreSQL 19 Beta 2 正式发布。这说明 PostgreSQL 的迭代速度并未放缓。从趋势看,接下来的版本可能聚焦于:

  • 更强的 AI/ML 原生支持:向量检索与 AI workload 的深度集成
  • 分区表增强:更大规模数据的高效管理
  • 增量检查点(Incremental Checkpoint):进一步减少 I/O 尖峰
  • 逻辑复制增强:多主复制能力的持续完善

PostgreSQL 早已不是那个「只有基本功能的开源数据库」,它在每一个版本中稳步演进,正在成为 AI 时代最可靠的数据基础设施之一。掌握每个版本的核心变化,是每个 DBA 和后端工程师的必修课。


本文基于 PostgreSQL 18.2 官方 Release Notes 编写,测试环境基于理论分析与公开基准测试。生产环境请以官方文档为准,并在非生产环境充分验证后再升级。

复制全文 生成海报 PostgreSQL 数据库 AIO SkipScan uuidv7 OAuth

推荐文章

liunx服务器监控workerman进程守护
2024-11-18 13:28:44 +0800 CST
动态渐变背景
2024-11-19 01:49:50 +0800 CST
程序员茄子在线接单