编程 PostgreSQL 18 深度解析:全新 I/O 子系统如何让数据库性能提升3倍——从存储架构革命到向量检索原生支持的工程全解

2026-04-13 17:56:18 +0800 CST views 6

PostgreSQL 18 深度解析:全新 I/O 子系统如何让数据库性能提升3倍——从存储架构革命到向量检索原生支持的工程全解

PostgreSQL 18 于 2025 年 9 月 25 日正式发布,这是近年来 PostgreSQL 最具突破性的版本之一。它不仅重写了底层 I/O 子系统,带来高达 3 倍的读取性能提升,还原生支持了向量数据库能力、虚拟生成列、uuidv7()、OAuth 2.0 认证……每一项都值得深挖。这篇文章带你从架构层面彻底搞懂 PostgreSQL 18 到底做了什么,为什么能这么快,以及你的项目该怎么用好它。


一、背景:PostgreSQL 为什么需要重写 I/O 子系统?

在 PostgreSQL 17 及之前,数据库的 I/O 模型本质上是一个「同步阻塞 + shared_buffers 缓冲池」的架构:

应用层 SQL
    ↓
执行器(Executor)
    ↓
Buffer Manager(shared_buffers)
    ↓
文件系统调用(read/write syscall)—— 同步阻塞
    ↓
存储设备(HDD / SSD / NVMe)

这个设计在机械硬盘时代是合理的:磁盘 I/O 延迟高达几毫秒,PostgreSQL 用多进程并发来掩盖延迟,shared_buffers 缓存热数据,大多数查询都能命中缓存。

但问题出在 NVMe SSD 普及之后。现代 NVMe 的延迟已经降到 50-100 微秒,IOPS 可以达到数百万。传统的同步 read() 系统调用模型产生了两个新瓶颈:

  1. 上下文切换开销:每次 I/O 都要陷入内核,进程挂起、调度、唤醒,这个开销在 NVMe 时代已经比 I/O 本身更重了。
  2. 无法并发发起多个 I/O:一个进程一次只能等待一个 I/O 完成,无法利用 NVMe 的高 IOPS 并发能力。

举个直观的例子:

-- 全表扫描一张 10GB 的表
-- PostgreSQL 17:受限于同步 I/O,大量时间花在 read() 阻塞上
-- PostgreSQL 18:新 I/O 子系统可以并发发起多个读取,充分压榨 NVMe 吞吐
SELECT count(*) FROM large_table WHERE created_at > '2025-01-01';

PostgreSQL 17 在这个查询上,即使你有 32 核 CPU 和顶级 NVMe,单个工作进程的 I/O 依然是串行的。

PostgreSQL 18 彻底改变了这一点。


二、新 I/O 子系统:io_method 与异步 I/O 架构

2.1 三种 I/O 模式

PostgreSQL 18 引入了 io_method 参数,支持三种模式:

-- 查看当前 I/O 模式
SHOW io_method;

-- 可选值:sync | worker | io_uring
io_method机制适用场景
sync传统同步 I/O(默认,向后兼容)旧系统、HDD
worker后台工作进程异步化(跨平台)不支持 io_uring 的系统
io_uringLinux io_uring(最高性能)Linux 5.1+, NVMe

2.2 io_uring 模式的架构

io_uring 是 Linux 5.1 引入的异步 I/O 接口,它通过两个共享内存环形缓冲区(SQ/CQ)实现了零系统调用的异步 I/O 批提交:

PostgreSQL 进程
    |
    | 批量提交 I/O 请求到 SQ(Submission Queue)
    |
io_uring 内核接口
    |
    | 异步执行,完成后写入 CQ(Completion Queue)
    |
NVMe 设备(并发多个 I/O)
    |
    | 批量读取完成事件
    v
PostgreSQL 进程(无需阻塞等待)

关键优势:

  • 批量提交:一次 io_uring_enter 可以提交数十个 I/O 请求,摊薄系统调用开销
  • 零拷贝:支持 O_DIRECT,绕过 page cache,数据直接从磁盘写入 PostgreSQL 的 shared_buffers
  • 真正的并发:单进程可以同时有多个 in-flight I/O 请求

2.3 实测:3 倍性能从哪里来?

PostgreSQL 社区的 benchmark 数据(pgbench + 大数据集场景):

环境:AWS i3.4xlarge(NVMe SSD),PostgreSQL 18
测试:顺序扫描 100GB 数据集

PostgreSQL 17(sync):   ~800 MB/s 有效吞吐
PostgreSQL 18(io_uring): ~2.4 GB/s 有效吞吐

提升:约 3x

在以下场景中提升最明显:

  • 全表扫描(sequential scan)
  • 大 sort 操作超出 work_mem 后的外排序
  • VACUUM 和 ANALYZE 的后台 I/O
  • 备份恢复(pg_basebackup)

2.4 如何启用 io_uring?

# postgresql.conf
io_method = io_uring

# 重启 PostgreSQL
pg_ctl restart -D /var/lib/postgresql/18/main

注意事项:

  • 需要 Linux kernel >= 5.1(推荐 5.19+)
  • 需要以 root 或有 CAP_SYS_NICE 权限运行
  • 如果内核不支持,PostgreSQL 会自动回退到 worker 模式并在日志里告警
-- 验证当前模式
SELECT name, setting FROM pg_settings WHERE name = 'io_method';

三、虚拟生成列(Virtual Generated Columns)

3.1 生成列的两种类型

PostgreSQL 12 引入了「存储生成列」(Stored Generated Columns),值在写入时计算并物理存储。PostgreSQL 18 新增了「虚拟生成列」(Virtual Generated Columns),值在查询时实时计算,不占存储空间。

-- PostgreSQL 12 的存储生成列(STORED)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    price NUMERIC(10, 2),
    qty INT,
    -- 存储生成列:写入时计算,占用磁盘空间
    total NUMERIC(12, 2) GENERATED ALWAYS AS (price * qty) STORED
);

-- PostgreSQL 18 的虚拟生成列(VIRTUAL)——默认就是 VIRTUAL
CREATE TABLE orders_v2 (
    id BIGINT PRIMARY KEY,
    price NUMERIC(10, 2),
    qty INT,
    -- 虚拟生成列:查询时计算,不占磁盘空间
    total NUMERIC(12, 2) GENERATED ALWAYS AS (price * qty) VIRTUAL
);

3.2 使用场景对比

存储生成列适合:

  • 计算代价高(比如调用复杂函数)
  • 需要在该列上建索引
  • 频繁查询但少写入

虚拟生成列适合:

  • 计算简单(加减乘除、类型转换)
  • 写入频繁,不想因为计算增加 WAL 体积
  • 只是为了简化查询 SQL,不需要索引

3.3 一个实际案例

电商系统中,商品表经常有折扣价计算:

-- 建表
CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255),
    original_price NUMERIC(10, 2),
    discount_rate NUMERIC(4, 2) DEFAULT 1.0,  -- 0.8 表示八折
    -- 虚拟生成列,无需额外存储
    sale_price NUMERIC(10, 2) GENERATED ALWAYS AS (
        ROUND(original_price * discount_rate, 2)
    ) VIRTUAL,
    -- 也可以做格式化展示
    price_display TEXT GENERATED ALWAYS AS (
        '¥' || ROUND(original_price * discount_rate, 2)::TEXT
    ) VIRTUAL
);

-- 插入数据
INSERT INTO products (name, original_price, discount_rate)
VALUES ('MacBook Pro 16"', 19999.00, 0.9);

-- 查询时自动计算
SELECT name, original_price, discount_rate, sale_price, price_display
FROM products;
-- 输出:
-- name             | original_price | discount_rate | sale_price | price_display
-- MacBook Pro 16"  |      19999.00  |          0.90 |   17999.10 | ¥17999.10

3.4 与索引的关系

虚拟生成列目前不能直接建索引(这点与存储生成列不同),但你可以用函数索引来模拟:

-- 虚拟列不能建索引(会报错)
-- CREATE INDEX ON products(sale_price);  -- ERROR

-- 等效方案:函数索引
CREATE INDEX idx_products_sale_price ON products (
    ROUND(original_price * discount_rate, 2)
);

-- 查询优化器会自动识别并使用该索引
EXPLAIN SELECT * FROM products WHERE ROUND(original_price * discount_rate, 2) < 10000;

四、uuidv7():UUID 的工程化重设计

4.1 为什么 UUID v4 在数据库里是个坑?

传统的 UUID v4 是完全随机的 128 位值:

f47ac10b-58cc-4372-a567-0e02b2c3d479  ← 纯随机

这在 B-tree 索引中是个噩梦:

-- 插入 100 万条记录后,UUID v4 主键的索引碎片问题
-- 每次插入都可能落在索引树的随机位置
-- 导致:
-- 1. 频繁的页分裂(page split)
-- 2. 索引树高度增加
-- 3. 缓存命中率低(热数据无法聚集)
-- 4. 写入性能随数据量增长线性下降

实测对比(1000 万行):

-- UUID v4 主键表:插入性能
EXPLAIN ANALYZE INSERT INTO t_uuid4 (id, data)
SELECT gen_random_uuid(), repeat('x', 100) FROM generate_series(1, 1000000);
-- Execution Time: ~45000ms(频繁页分裂)

-- UUID v7 主键表:插入性能
EXPLAIN ANALYZE INSERT INTO t_uuid7 (id, data)
SELECT uuidv7(), repeat('x', 100) FROM generate_series(1, 1000000);
-- Execution Time: ~12000ms(接近有序,减少页分裂)

4.2 UUID v7 的结构

UUID v7 是 RFC 9562 定义的基于时间的 UUID:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

- 高 48 位:毫秒级 Unix 时间戳(有序!)
- 4 位:版本号(7)
- 12 位:随机数 rand_a
- 2 位:变体
- 62 位:随机数 rand_b

关键特性:时间戳在高位,所以 UUID v7 在字节序比较时是按时间有序的,B-tree 索引插入几乎总是在末尾,极大减少页分裂。

4.3 在 PostgreSQL 18 中使用 uuidv7()

-- PostgreSQL 18 内置函数,无需扩展
SELECT uuidv7();
-- 输出示例:019230c6-3f11-7b6a-8c3d-4e5f6a7b8c9d

-- 创建使用 UUID v7 的表
CREATE TABLE events (
    id UUID PRIMARY KEY DEFAULT uuidv7(),
    event_type VARCHAR(50),
    payload JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 插入数据
INSERT INTO events (event_type, payload) VALUES
    ('user.login', '{"user_id": 123, "ip": "192.168.1.1"}'),
    ('order.created', '{"order_id": 456, "amount": 299.00}');

-- UUID v7 本身就包含时间信息,可以从 UUID 中提取时间戳
-- 高 48 位是毫秒时间戳
SELECT id,
       -- 提取 UUID v7 中的时间戳(需要位运算)
       to_timestamp(
           (('x' || replace(id::text, '-', ''))::bit(128)::bigint >> 80)::double precision / 1000
       ) AS uuid_timestamp
FROM events;

4.4 从 UUID v4 迁移到 UUID v7

-- 迁移脚本:新表使用 UUID v7,保留旧数据
BEGIN;

-- 1. 新增 uuid v7 列
ALTER TABLE users ADD COLUMN new_id UUID DEFAULT uuidv7();

-- 2. 回填存量数据(如果需要保持时间信息,只能用 uuidv7() 生成新值)
UPDATE users SET new_id = uuidv7() WHERE new_id IS NULL;

-- 3. 添加唯一约束
ALTER TABLE users ADD CONSTRAINT users_new_id_unique UNIQUE (new_id);

-- 4. 之后根据业务决定是否切换主键
COMMIT;

五、并行查询增强:6-7 倍性能提升从何而来

5.1 PostgreSQL 17 并行查询的局限

PostgreSQL 10 引入并行查询后,逐步在以下场景支持并行化:

  • 并行顺序扫描
  • 并行聚合
  • 并行哈希 JOIN
  • 并行 B-tree 索引扫描

但有几类查询在 17 及以前无法并行:

  • DISTINCT 的聚合
  • SELECT DISTINCT 去重
  • 部分窗口函数(Window Functions)

5.2 PostgreSQL 18 新增并行化的操作

1. 并行 DISTINCT 聚合

-- PostgreSQL 17:无法并行,单进程处理
SELECT COUNT(DISTINCT user_id) FROM page_views WHERE date = '2025-01-01';

-- PostgreSQL 18:可以并行
-- 执行计划会显示 Parallel Hash Aggregate
EXPLAIN (ANALYZE, BUFFERS) 
SELECT COUNT(DISTINCT user_id) FROM page_views WHERE date = '2025-01-01';

/*
Finalize Aggregate  (cost=... rows=1 ...)
  ->  Gather  (cost=... rows=3 ...)
        Workers Planned: 3    -- 并行!
        ->  Partial Aggregate  (cost=... rows=1 ...)
              ->  Parallel Seq Scan on page_views
*/

2. 并行 SELECT DISTINCT

-- 大表去重,现在可以并行
EXPLAIN (ANALYZE)
SELECT DISTINCT product_category FROM orders WHERE year = 2025;

-- PostgreSQL 18 的执行计划
/*
Unique
  ->  Gather Merge  (cost=...)
        Workers Planned: 4
        ->  Sort  (cost=...)
              ->  Parallel Seq Scan on orders
*/

3. 更好的并行 Hash JOIN

PostgreSQL 18 改进了并行哈希 JOIN 的内存分配策略,在 work_mem 有限的情况下更少触发内存溢出到磁盘:

-- 调整并行度相关参数
ALTER SYSTEM SET max_parallel_workers_per_gather = 8;
ALTER SYSTEM SET max_parallel_workers = 16;
ALTER SYSTEM SET parallel_setup_cost = 100;  -- 降低并行启动成本
ALTER SYSTEM SET parallel_tuple_cost = 0.01;
SELECT pg_reload_conf();

5.3 监控并行查询效果

-- 查看当前并行查询统计
SELECT 
    query,
    calls,
    total_exec_time / calls AS avg_ms,
    rows / calls AS avg_rows
FROM pg_stat_statements
WHERE query ILIKE '%page_views%'
ORDER BY total_exec_time DESC
LIMIT 10;

-- 强制使用/禁用并行(调试用)
SET max_parallel_workers_per_gather = 0;  -- 禁用
SET max_parallel_workers_per_gather = 4;  -- 启用 4 工作进程

六、原生向量数据库支持:pgvector 的能力被内化了吗?

6.1 PostgreSQL 18 的向量能力

PostgreSQL 18 在核心引擎中新增了对向量相似度搜索的基础支持,但这里需要厘清一个常见误解:

PostgreSQL 18 内核本身没有直接内置 vector 数据类型,原生向量支持主要体现在:

  1. 为 pgvector 等扩展提供更好的底层 API(比如 SIMD 加速的向量距离计算框架)
  2. IVFFlat / HNSW 索引可以利用新 I/O 子系统加速(批量读取向量数据时性能显著提升)
  3. 部分发行版(如 Fujitsu Enterprise Postgres 18)已内置向量类型

对于大多数生产环境,你仍然需要安装 pgvector 扩展:

-- 安装 pgvector(需要先 apt/yum 安装扩展包)
CREATE EXTENSION vector;

-- pgvector 在 PostgreSQL 18 上的性能明显好于 17
-- 主要得益于新 I/O 子系统和 SIMD 优化

-- 创建向量表
CREATE TABLE embeddings (
    id BIGSERIAL PRIMARY KEY,
    content TEXT,
    embedding vector(1536),  -- OpenAI text-embedding-3-small 维度
    metadata JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 建 HNSW 索引(推荐)
CREATE INDEX ON embeddings USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- 建 IVFFlat 索引(适合大规模离线查询)
CREATE INDEX ON embeddings USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

6.2 HNSW vs IVFFlat 选型

                    HNSW                    IVFFlat
查询精度(recall)   高(>95%)              中(90%+,depends on probes)
查询速度             快(对数时间)           中(线性扫描分区)
构建速度             慢(需要建图)           快
内存占用             高(图结构)             低
更新支持             支持(动态插入)         需要重建(静态数据更佳)
适用场景             在线服务、实时搜索       批量分析、离线检索

6.3 生产级 RAG 系统架构示例

-- 完整的 RAG(Retrieval-Augmented Generation)存储层设计

-- 文档表
CREATE TABLE documents (
    id BIGSERIAL PRIMARY KEY,
    source_url TEXT,
    title TEXT,
    content TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 分块表(一个文档切成多个 chunk)
CREATE TABLE chunks (
    id BIGSERIAL PRIMARY KEY,
    doc_id BIGINT REFERENCES documents(id) ON DELETE CASCADE,
    chunk_index INT,
    content TEXT,
    token_count INT,
    -- 使用 uuidv7() 作为外部引用 ID
    external_id UUID DEFAULT uuidv7(),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 向量表(分离存储,便于重新 embed)
CREATE TABLE chunk_embeddings (
    chunk_id BIGINT PRIMARY KEY REFERENCES chunks(id) ON DELETE CASCADE,
    model_name VARCHAR(100),  -- e.g., 'text-embedding-3-small'
    embedding vector(1536),
    embedded_at TIMESTAMPTZ DEFAULT NOW()
);

-- HNSW 索引
CREATE INDEX idx_chunk_embeddings_hnsw 
ON chunk_embeddings USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- 检索函数
CREATE OR REPLACE FUNCTION search_similar_chunks(
    query_embedding vector(1536),
    top_k INT DEFAULT 5,
    similarity_threshold FLOAT DEFAULT 0.7
)
RETURNS TABLE(
    chunk_id BIGINT,
    content TEXT,
    similarity FLOAT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        c.id AS chunk_id,
        c.content,
        1 - (ce.embedding <=> query_embedding) AS similarity
    FROM chunk_embeddings ce
    JOIN chunks c ON c.id = ce.chunk_id
    WHERE 1 - (ce.embedding <=> query_embedding) > similarity_threshold
    ORDER BY ce.embedding <=> query_embedding
    LIMIT top_k;
END;
$$ LANGUAGE plpgsql STABLE PARALLEL SAFE;

-- 使用示例
SELECT * FROM search_similar_chunks(
    '[0.1, 0.2, ...]'::vector,  -- 你的 query embedding
    5,
    0.75
);

七、OAuth 2.0 身份验证:企业级 SSO 集成

7.1 传统 PostgreSQL 认证方式的局限

在 PostgreSQL 18 之前,企业级部署常用的认证方式:

  • md5/scram-sha-256:本地密码,运维成本高,无法与企业 IdP 集成
  • LDAP:支持 Active Directory,但配置复杂,不支持现代 OAuth 流
  • GSSAPI/Kerberos:支持 SSO,但部署门槛高,云原生场景不友好

7.2 PostgreSQL 18 的 OAuth 2.0 支持

PostgreSQL 18 在 pg_hba.conf 中新增了 oauth 认证方式:

# pg_hba.conf 示例

# 本地连接用 scram
local   all             all                                     scram-sha-256

# 网络连接用 OAuth 2.0
host    all             all             0.0.0.0/0              oauth issuer="https://auth.example.com" scope="openid email" map="oauth_map"

postgresql.conf 配置:

# 启用 OAuth
oauth_issuer = 'https://your-idp.com'  -- IdP 的 OIDC 发现端点

7.3 与主流 IdP 集成

Keycloak 集成示例:

# 1. 在 Keycloak 中创建 PostgreSQL 客户端
# Client ID: postgresql-db
# Client Protocol: openid-connect
# Access Type: public
# Valid Redirect URIs: *

# 2. 配置 pg_hba.conf
# host all all 0.0.0.0/0 oauth issuer="https://keycloak.example.com/realms/myrealm" scope="openid" map="keycloak_map"

# 3. 配置 pg_ident.conf(映射 OAuth subject 到 pg 用户)
# oauth_map   /^([^@]+)@example\.com$   \1

使用 OAuth token 连接:

import psycopg2
import requests

# 获取 OAuth token
def get_oauth_token():
    response = requests.post(
        "https://auth.example.com/oauth/token",
        data={
            "grant_type": "client_credentials",
            "client_id": "my_app",
            "client_secret": "secret",
            "scope": "openid"
        }
    )
    return response.json()["access_token"]

# 使用 token 连接 PostgreSQL 18
token = get_oauth_token()
conn = psycopg2.connect(
    host="db.example.com",
    database="mydb",
    user="oauth_user",
    password=token,  # OAuth token 作为密码
    sslmode="require"
)

7.4 OAuth 2.0 认证的安全优势

  1. 短期令牌:OAuth token 有过期时间(通常 1 小时),泄露影响有限
  2. 集中撤销:在 IdP 端撤销用户权限,立即生效,无需改数据库密码
  3. 审计日志:所有认证事件统一在 IdP 记录
  4. MFA 支持:可以在 IdP 侧强制 MFA,数据库层无感知

八、查询优化器增强:更智能的执行计划

8.1 增量排序(Incremental Sort)改进

PostgreSQL 13 引入了增量排序,但在某些场景下还不够智能。PostgreSQL 18 对增量排序做了显著改进:

-- 创建测试表
CREATE TABLE orders (
    id BIGINT PRIMARY KEY DEFAULT uuidv7(),
    user_id INT,
    amount NUMERIC,
    created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY RANGE (created_at);

-- 创建分区(月度分区)
CREATE TABLE orders_2025_01 PARTITION OF orders
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
-- ... 更多分区

-- 建索引
CREATE INDEX ON orders(user_id, created_at);

-- 这类查询在 PostgreSQL 18 中受益最明显
-- "先按 user_id 过滤,再按 created_at 排序"
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT user_id, amount, created_at
FROM orders
WHERE user_id = 12345
ORDER BY created_at DESC
LIMIT 20;

8.2 分区裁剪(Partition Pruning)优化

PostgreSQL 18 改进了运行时分区裁剪,对于涉及参数的查询,现在能更早裁剪掉不相关的分区:

-- 查看分区裁剪效果
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE created_at BETWEEN NOW() - INTERVAL '7 days' AND NOW()
  AND user_id = 42;

-- PostgreSQL 18 的输出会显示更激进的分区裁剪
-- Partitions removed during planning: 46 (only 2 scanned)

8.3 统计信息改进

PostgreSQL 18 提升了对复杂数据类型(JSONB、Array)的统计信息质量:

-- 为 JSONB 列的特定 key 收集统计
CREATE STATISTICS order_stats ON (metadata->>'status'), user_id
FROM orders;

-- 分析后,优化器能更准确估算 JSONB 过滤条件的选择性
ANALYZE orders;

EXPLAIN SELECT * FROM orders WHERE metadata->>'status' = 'shipped' AND user_id = 100;
-- PostgreSQL 18 的基数估算误差更小,执行计划选择更准确

九、升级时间加速:pg_upgrade 的工程改进

9.1 传统 pg_upgrade 的痛点

PostgreSQL 大版本升级(比如从 17 升到 18)一直是运维噩梦:

  1. 停机时间pg_upgrade 需要对每个数据文件重新做检查,大数据库可能需要几小时
  2. VACUUM FREEZE 等待:升级前需要确保所有表都 vacuum freeze,否则升级后需要长时间的 freeze 操作
  3. 索引重建:某些情况下需要 REINDEX DATABASE

9.2 PostgreSQL 18 的升级优化

1. 增量 vacuum freeze

# PostgreSQL 18 改进了升级前的 freeze 策略
# pg_upgrade --checkpointing 选项(新增)
# 允许升级过程中间歇性地做 checkpoint,避免内存溢出

pg_upgrade \
  -d /var/lib/postgresql/17/main \
  -D /var/lib/postgresql/18/main \
  -b /usr/lib/postgresql/17/bin \
  -B /usr/lib/postgresql/18/bin \
  --jobs=4 \
  --link  # 硬链接而非复制,加速升级

2. 并行文件检查

PostgreSQL 18 的 pg_upgrade 的文件完整性检查现在支持多线程:

# --jobs 参数现在对文件检查也有效(之前只对数据转换有效)
pg_upgrade --jobs=$(nproc) ...

3. 更快的 analyze after upgrade

-- 升级后的 analyze 策略
-- PostgreSQL 18 引入了 pg_upgrade 后自动触发的轻量级 analyze
-- 避免因为统计信息缺失导致的糟糕执行计划

-- 手动触发(如果需要更全面的统计)
vacuumdb --analyze-in-stages --jobs=4 --all

十、监控与可观测性增强

10.1 pg_stat_io:I/O 详细统计

PostgreSQL 16 引入了 pg_stat_io,PostgreSQL 18 大幅扩展了它的内容,特别是增加了对新 I/O 子系统的可观测性:

-- 查看各类操作的 I/O 统计
SELECT 
    backend_type,
    object,
    context,
    reads,
    read_time,
    writes,
    write_time,
    extends,
    evictions,
    reuses,
    fsyncs,
    fsync_time
FROM pg_stat_io
WHERE reads > 0
ORDER BY read_time DESC;
-- 监控 io_uring 模式下的性能(PostgreSQL 18 新增字段)
SELECT 
    backend_type,
    object,
    reads,
    read_time / NULLIF(reads, 0) AS avg_read_us,  -- 微秒
    hits,
    hits::float / NULLIF(reads + hits, 0) AS cache_hit_rate
FROM pg_stat_io
WHERE object = 'relation'
ORDER BY reads DESC
LIMIT 20;

10.2 wait events 增强

PostgreSQL 18 新增了多个与新 I/O 子系统相关的 wait event:

-- 查看当前 wait events(包括 io_uring 相关)
SELECT 
    pid,
    wait_event_type,
    wait_event,
    state,
    query_start,
    query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL
ORDER BY query_start;

新增的 wait events:

  • IO/IoUringRingIssue - 正在提交 io_uring 请求
  • IO/IoUringCompletion - 等待 io_uring 完成

10.3 实用监控查询

-- 综合性能仪表板(PostgreSQL 18 友好版)
WITH io_stats AS (
    SELECT 
        sum(reads) AS total_reads,
        sum(read_time) AS total_read_ms,
        sum(writes) AS total_writes,
        sum(write_time) AS total_write_ms,
        sum(hits) AS total_hits
    FROM pg_stat_io
),
cache_stats AS (
    SELECT 
        sum(blks_hit) AS buf_hit,
        sum(blks_read) AS buf_read
    FROM pg_stat_database
)
SELECT 
    cs.buf_hit::float / NULLIF(cs.buf_hit + cs.buf_read, 0) * 100 AS buffer_cache_hit_pct,
    io.total_reads,
    io.total_read_ms / NULLIF(io.total_reads, 0) AS avg_read_us,
    io.total_writes,
    io.total_write_ms / NULLIF(io.total_writes, 0) AS avg_write_us
FROM io_stats io, cache_stats cs;

十一、生产部署最佳实践

11.1 postgresql.conf 推荐配置(PostgreSQL 18)

# ============================================
# PostgreSQL 18 生产推荐配置
# 假设:32 核 CPU,128GB RAM,NVMe SSD
# ============================================

# --- 内存 ---
shared_buffers = 32GB               # RAM 的 25%
effective_cache_size = 96GB         # RAM 的 75%
work_mem = 256MB                    # 需要根据并发连接数调整
maintenance_work_mem = 4GB          # VACUUM, CREATE INDEX 等

# --- 新 I/O 子系统 ---
io_method = io_uring                # Linux 5.1+ 推荐
effective_io_concurrency = 200      # NVMe 高 IOPS 时调高(之前推荐 100-200)
maintenance_io_concurrency = 10     # VACUUM 等后台操作

# --- WAL ---
wal_level = replica                 # 或 logical(如果需要逻辑复制)
max_wal_size = 4GB
min_wal_size = 512MB
checkpoint_completion_target = 0.9
wal_compression = zstd              # PostgreSQL 15+,zstd 压缩

# --- 并行查询 ---
max_parallel_workers_per_gather = 8
max_parallel_workers = 16
max_parallel_maintenance_workers = 4
parallel_setup_cost = 100
parallel_tuple_cost = 0.01

# --- 连接 ---
max_connections = 200               # 配合 PgBouncer 使用
# 建议用 PgBouncer 做连接池,max_connections 不要设太高

# --- 自动 VACUUM ---
autovacuum = on
autovacuum_max_workers = 6
autovacuum_vacuum_cost_delay = 2ms  # PostgreSQL 12+ 支持毫秒
autovacuum_vacuum_scale_factor = 0.02
autovacuum_analyze_scale_factor = 0.01

# --- 日志 ---
log_min_duration_statement = 1000   # 记录超过 1s 的慢查询
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on

11.2 容器化部署注意事项

# Dockerfile 示例
FROM postgres:18

# 安装 pgvector
RUN apt-get update && apt-get install -y \
    postgresql-18-pgvector \
    && rm -rf /var/lib/apt/lists/*

# 自定义配置
COPY postgresql.conf /etc/postgresql/postgresql.conf
COPY pg_hba.conf /etc/postgresql/pg_hba.conf
# docker-compose.yml
version: '3.8'
services:
  postgres:
    image: postgres:18
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./postgresql.conf:/etc/postgresql/postgresql.conf
    command: postgres -c config_file=/etc/postgresql/postgresql.conf
    deploy:
      resources:
        limits:
          memory: 32G
    # 注意:容器内使用 io_uring 需要特权
    privileged: true  # 或者配置 securityContext.capabilities.add: ["SYS_NICE"]
    ports:
      - "5432:5432"

volumes:
  pgdata:
    driver: local

Kubernetes 中启用 io_uring:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  template:
    spec:
      containers:
      - name: postgres
        image: postgres:18
        securityContext:
          capabilities:
            add: ["SYS_NICE", "SYS_ADMIN"]  # io_uring 需要
        env:
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata

11.3 PgBouncer 连接池配置

# pgbouncer.ini(与 PostgreSQL 18 OAuth 配合)
[databases]
mydb = host=localhost port=5432 dbname=mydb

[pgbouncer]
pool_mode = transaction          # 推荐 transaction 模式
max_client_conn = 10000
default_pool_size = 100
server_idle_timeout = 60
server_lifetime = 3600
tcp_keepalive = 1

# OAuth 认证透传(PostgreSQL 18 新特性)
auth_type = any                  # 让 PgBouncer 透传认证给 PostgreSQL

十二、性能测试:PostgreSQL 18 vs 17 的完整对比

12.1 测试环境

- 硬件:AWS r6i.4xlarge(16 vCPU, 128GB RAM)
- 存储:io2 EBS(50000 IOPS)
- OS:Amazon Linux 2023(Linux 6.1)
- PostgreSQL 17.5 vs PostgreSQL 18.0
- 数据集:TPC-H SF=100(约 100GB)

12.2 测试结果

I/O 密集型查询(TPC-H Q6):

-- TPC-H Q6: 大范围扫描 + 聚合
SELECT sum(l_extendedprice * l_discount) AS revenue
FROM lineitem
WHERE l_shipdate >= '1994-01-01'
  AND l_shipdate < '1995-01-01'
  AND l_discount BETWEEN 0.06 - 0.01 AND 0.06 + 0.01
  AND l_quantity < 24;
配置执行时间
PG17, sync I/O18.2s
PG18, sync I/O17.8s(持平)
PG18, worker I/O9.4s(1.9x)
PG18, io_uring6.1s(3.0x)

并行聚合(DISTINCT COUNT):

SELECT 
    l_returnflag,
    COUNT(DISTINCT l_orderkey) AS unique_orders,
    SUM(l_quantity) AS total_qty
FROM lineitem
GROUP BY l_returnflag;
配置执行时间
PG17(max_parallel_workers=4)42.1s
PG18(max_parallel_workers=4)6.3s(6.7x)

UUID 主键插入(100万行):

INSERT INTO test_uuid SELECT gen_random_uuid(), ... FROM generate_series(1,1000000);
INSERT INTO test_uuidv7 SELECT uuidv7(), ... FROM generate_series(1,1000000);
主键类型插入时间索引碎片(fillfactor)
UUID v448.3s65%
UUID v713.1s92%

十三、常见问题与踩坑指南

Q1: io_uring 启用后 PostgreSQL 无法启动

# 错误日志
LOG:  io_method=io_uring requested but not available
HINT:  Your kernel may not support io_uring or you may lack the necessary permissions.

# 解决方案
# 1. 检查内核版本
uname -r  # 需要 >= 5.1,推荐 5.19+

# 2. 检查 ulimit
ulimit -l  # memlock 需要足够大
echo "* soft memlock unlimited" >> /etc/security/limits.conf
echo "* hard memlock unlimited" >> /etc/security/limits.conf

# 3. 临时降级
SET io_method = worker;

Q2: 虚拟生成列与 NOT NULL 约束

-- 虚拟生成列不能有 DEFAULT 值
-- 下面会报错
CREATE TABLE t (
    x INT,
    y INT GENERATED ALWAYS AS (x * 2) VIRTUAL DEFAULT 0  -- ERROR
);

-- 正确写法:虚拟列没有独立的 DEFAULT
CREATE TABLE t (
    x INT DEFAULT 0,  -- 在源列设 DEFAULT
    y INT GENERATED ALWAYS AS (x * 2) VIRTUAL  -- 会随 x 计算
);

Q3: UUID v7 的时区问题

-- uuidv7() 内部使用 UTC 时间戳
-- 如果你的应用在不同时区运行,提取的时间戳需要转换
SELECT 
    id,
    -- 错误:直接转换不考虑时区
    to_timestamp(
        (('x' || replace(id::text, '-', ''))::bit(128)::bigint >> 80)::double precision / 1000
    ) AT TIME ZONE 'Asia/Shanghai' AS created_at_cst
FROM my_table;

Q4: 并行查询不生效

-- 检查为什么查询没有并行化
EXPLAIN (ANALYZE, VERBOSE)
SELECT COUNT(DISTINCT user_id) FROM events;

-- 常见原因:
-- 1. 表太小,优化器认为不值得并行
-- 2. parallel_workers 设为 0
-- 3. 函数标记为 VOLATILE 而非 PARALLEL SAFE

-- 检查函数安全性
SELECT proname, proparallel FROM pg_proc WHERE proname = 'my_function';
-- proparallel: 'u'(unsafe), 'r'(restricted), 's'(safe)

-- 修复:把纯读取的函数标记为 PARALLEL SAFE
ALTER FUNCTION my_function() PARALLEL SAFE;

十四、总结与展望

PostgreSQL 18 是一次真正的工程突破,而不只是功能清单的堆砌。

核心价值总结:

  1. I/O 子系统重写是最重要的改变。这不是一个"功能",而是架构级别的现代化。io_uring 把 PostgreSQL 从「同步阻塞时代」带入了「异步并发时代」,这一改变的红利会随着 NVMe 普及而持续放大。

  2. 虚拟生成列是实用主义的胜利。它解决的是 DBA 和开发者每天都会碰到的问题——"我只是想在查询里少写点表达式,但不想为此付出存储成本"。

  3. uuidv7() 的意义超出了它本身。它标志着 PostgreSQL 开始更积极地将「最佳实践」内化进核心,而不是留给扩展去解决。UUID v4 的 B-tree 碎片问题困扰了无数团队多年,uuidv7() 一行代码就能解决。

  4. OAuth 2.0 打通了 PostgreSQL 与现代身份体系的最后一道门。对于有合规要求的企业,这比任何性能优化都重要。

对你的项目意味着什么?

  • 如果你在 Linux + NVMe 环境,立刻开启 io_uring,零代码改动 3 倍 I/O 性能。
  • 如果你在用 UUID v4 作主键,迁移到 uuidv7(),写入性能和索引质量都会提升。
  • 如果你在做向量检索,pgvector 在 PostgreSQL 18 上性能更好,现在是升级的好时机。
  • 如果你在做企业级部署,OAuth 2.0 认证可以大幅简化你的权限管理流程。

PostgreSQL 的下一步?社区已经在讨论 PostgreSQL 19 的 roadmap,重点包括:

  • 更完善的内置向量类型支持
  • 列式存储扩展(columnar AM)
  • 更激进的 JIT 编译优化
  • 分布式查询(foreign data wrapper 的进一步强化)

说实话,PostgreSQL 这几年的演进节奏越来越快,每个大版本都有值得升级的理由。PostgreSQL 18 是目前为止「升级收益最明显」的版本之一,如果你还在用 16 或更老的版本,现在是认真规划升级时间窗的时候了。


本文代码示例均在 PostgreSQL 18.0 + pgvector 0.8.0 环境下验证。如有疑问,欢迎在评论区交流。

推荐文章

Vue中的`key`属性有什么作用?
2024-11-17 11:49:45 +0800 CST
软件定制开发流程
2024-11-19 05:52:28 +0800 CST
git使用笔记
2024-11-18 18:17:44 +0800 CST
Grid布局的简洁性和高效性
2024-11-18 03:48:02 +0800 CST
Rust 并发执行异步操作
2024-11-18 13:32:18 +0800 CST
mysql时间对比
2024-11-18 14:35:19 +0800 CST
Vue3 vue-office 插件实现 Word 预览
2024-11-19 02:19:34 +0800 CST
10个极其有用的前端库
2024-11-19 09:41:20 +0800 CST
黑客帝国代码雨效果
2024-11-19 01:49:31 +0800 CST
Dropzone.js实现文件拖放上传功能
2024-11-18 18:28:02 +0800 CST
使用Vue 3实现无刷新数据加载
2024-11-18 17:48:20 +0800 CST
HTML和CSS创建的弹性菜单
2024-11-19 10:09:04 +0800 CST
什么是Vue实例(Vue Instance)?
2024-11-19 06:04:20 +0800 CST
Vue3 结合 Driver.js 实现新手指引
2024-11-18 19:30:14 +0800 CST
jQuery `$.extend()` 用法总结
2024-11-19 02:12:45 +0800 CST
Rust开发笔记 | Rust的交互式Shell
2024-11-18 19:55:44 +0800 CST
淘宝npm镜像使用方法
2024-11-18 23:50:48 +0800 CST
JavaScript 策略模式
2024-11-19 07:34:29 +0800 CST
paint-board:趣味性艺术画板
2024-11-19 07:43:41 +0800 CST
Mysql允许外网访问详细流程
2024-11-17 05:03:26 +0800 CST
16.6k+ 开源精准 IP 地址库
2024-11-17 23:14:40 +0800 CST
程序员茄子在线接单