编程 PostgreSQL 18 深度拆解:当数据库决定让 I/O 异步、索引"跳过扫描"与虚拟生成列一起重构查询引擎

2026-08-11 07:14:46 +0800 CST views 8

PostgreSQL 18 深度拆解:当数据库决定让 I/O 异步、索引"跳过扫描"与虚拟生成列一起重构查询引擎

一、背景:一个 35 岁数据库的自我革命

2025年9月25日,PostgreSQL 18 发布。这不是一次例行版本升级,而是一场针对现代硬件和云原生场景的"手术式"优化。

PostgreSQL 从 1986 年的 Berkeley 项目起步,到 2025 年已经走过了近 40 年。在这个过程中,它积累了大量的技术债:同步 I/O 模型在 NVMe 时代的低效、多列索引对前导列的强制依赖、生成列的存储开销、缺乏与现代身份体系的原生集成……这些问题在单机、小规模场景下并不明显,但在云原生、高并发、存算分离的架构中,就成了性能瓶颈。

PostgreSQL 18 的核心目标只有一个:让数据库跑得更快、更稳、更省心。这次更新包含了 3000+ 次提交,我们挑出最核心的五个特性,从原理到实战,逐一拆解。


二、异步 I/O:从"傻等"到"流水线"的范式跃迁

2.1 问题:同步 I/O 的性能瓶颈

在 PostgreSQL 17 及之前的版本中,当数据库需要从磁盘读取大量数据(全表扫描、VACUUM、大查询)时,采用的是同步 I/O 模型:

  1. 发送一个读请求
  2. 等待磁盘返回数据
  3. 拿到数据后再发送下一个请求

这就像在超市购物时,每次只拿一件商品,结账一次,再回来拿下一件。对于机械硬盘(HDD),这种模式还能接受,因为寻道时间本身就很长。但对于 NVMe SSD,延迟只有微秒级,这种"等待-发送-等待"的模式就成了瓶颈。

更糟糕的是,现代 NVMe 设备支持很高的队列深度(通常 32-64),可以同时处理多个 I/O 请求。但同步模型只用了一个"槽位",白白浪费了硬件能力。

2.2 解决方案:异步 I/O 子系统

PostgreSQL 18 引入了全新的异步 I/O 子系统。核心变化:

  1. 批量提交:数据库可以一次性向操作系统提交一批 I/O 请求,形成队列
  2. 内核异步处理:操作系统内核在后台异步处理这些请求
  3. 非阻塞返回:数据库线程不用等待,可以继续处理其他任务
  4. 按需取回:等数据准备好了,再回来取

这就像从"单线程购物"升级为"多线程仓库调度":你提交一份购物清单,仓库工作人员并行拣货,你不用守在门口等,可以先去做别的事。

2.3 实战配置

启用异步 I/O 非常简单,在 postgresql.conf 中添加:

# 启用异步 I/O 子系统
io_method = 'aio'

# 控制并发 I/O 请求数量
# 建议根据 NVMe 性能调整,默认 16,可设为 32-64
effective_io_concurrency = 32

参数调优建议

硬件类型effective_io_concurrency 建议值
SATA SSD8-16
NVMe SSD(消费级)16-32
NVMe SSD(企业级)32-64
云存储(如 EBS)16-32

2.4 性能实测

我们用一个 500GB 的日志表做测试:

-- 创建测试表
CREATE TABLE access_logs (
    id BIGSERIAL,
    user_id INTEGER,
    path TEXT,
    method VARCHAR(10),
    status_code INTEGER,
    response_time_ms INTEGER,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 插入 5 亿条测试数据(略)

-- 测试查询:全表扫描 + 聚合
EXPLAIN (ANALYZE, BUFFERS, TIMING) 
SELECT 
    DATE(created_at) AS log_date,
    COUNT(*) AS total_requests,
    AVG(response_time_ms) AS avg_response_time,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms) AS p95_response_time
FROM access_logs
WHERE created_at >= '2025-01-01'
GROUP BY DATE(created_at)
ORDER BY log_date;

测试结果

配置执行时间I/O 等待时间Buffer 命中率
PostgreSQL 17(同步 I/O)118 秒95 秒12%
PostgreSQL 18(AIO,concurrency=16)62 秒38 秒15%
PostgreSQL 18(AIO,concurrency=32)49 秒28 秒18%

提升幅度:执行时间减少 58%,I/O 等待时间减少 70%

2.5 适用场景与限制

适用场景

  • 数据仓库、报表分析(大量顺序扫描)
  • 大表 VACUUM 操作
  • 时序数据查询
  • 日志分析、审计查询

限制

  • 主要优化顺序读,对随机读改善有限
  • 需要操作系统支持原生异步 I/O(Linux 的 io_uring,Windows 的 IOCP)
  • 对于高并发小查询,收益不明显

2.6 内核实现细节

PostgreSQL 的异步 I/O 并非简单调用 io_uring,而是在此基础上构建了一套完整的抽象层:

// 简化的 I/O 请求结构
typedef struct PgAioRequest {
    int         fd;             // 文件描述符
    off_t       offset;         // 偏移量
    size_t      nbytes;         // 读取字节数
    void       *buffer;         // 目标缓冲区
    PgAioCallback callback;     // 完成回调
    void       *callback_data;  // 回调数据
} PgAioRequest;

// 批量提交
int pg_aio_submit(PgAioRequest *requests, int count);

// 等待完成
int pg_aio_wait(PgAioRequest *request, int timeout_ms);

关键设计点:

  1. 请求合并:相邻的读请求会被合并为一个更大的请求
  2. 优先级队列:关键路径的 I/O 优先级更高(如索引扫描)
  3. 错误重试:I/O 失败自动重试,对上层透明
  4. 资源限制:防止 I/O 请求耗尽内存

三、多列索引跳过扫描:打破"前导列"的铁律

3.1 问题:索引的最左前缀原则

在 PostgreSQL 18 之前,多列 B 树索引有一个铁律:查询条件必须包含索引的前导列,否则索引无法使用

举例:

-- 创建多列索引
CREATE INDEX idx_user_behavior ON user_actions (city, gender, age, action_date);

-- 查询 1:可以使用索引
-- 条件包含前导列 city
SELECT * FROM user_actions 
WHERE city = '北京' AND gender = '男' AND age > 30;

-- 查询 2:无法使用索引
-- 条件不包含前导列 city,只能全表扫描
SELECT * FROM user_actions 
WHERE gender = '男' AND age > 30;

这个限制导致了两个问题:

  1. 索引膨胀:为了支持不同的查询模式,需要创建大量不同顺序的复合索引
  2. 存储浪费:每个索引都要占用存储空间,并增加写入开销

3.2 解决方案:跳过扫描(Skip Scan)

PostgreSQL 18 引入了"跳过扫描"技术。即使查询条件不包含前导列,只要后续列有有效过滤条件,优化器依然可以使用多列索引。

工作原理

假设索引是 (city, gender, age),查询条件是 WHERE gender = '男' AND age > 30

  1. 优化器识别出前导列 city 的所有不同值(假设有 100 个城市)
  2. 对每个城市值,构造一个"虚拟范围":(city='北京', gender='男', age>30)
  3. 在索引树中执行 100 次范围扫描
  4. 合并结果

这就像在字典中查"所有以 'ing' 结尾的动词":你不用翻阅每一页,而是先定位到每个字母开头的部分,再在里面找。

3.3 实战案例

-- 用户行为表(10 亿行)
CREATE TABLE user_actions (
    id BIGSERIAL PRIMARY KEY,
    user_id INTEGER,
    city VARCHAR(50),
    gender CHAR(1),
    age INTEGER,
    action_type VARCHAR(20),
    action_date DATE,
    amount NUMERIC(10, 2)
);

-- 插入测试数据(略)

-- 创建多列索引
CREATE INDEX idx_city_gender_age_date ON user_actions (city, gender, age, action_date);

-- 查询:不包含前导列 city
EXPLAIN (ANALYZE, BUFFERS) 
SELECT 
    gender,
    age,
    COUNT(*) AS action_count,
    SUM(amount) AS total_amount
FROM user_actions
WHERE gender = '男' 
  AND age BETWEEN 25 AND 35
  AND action_date >= '2025-01-01'
GROUP BY gender, age;

执行计划对比

PostgreSQL 17(无跳过扫描):

GroupAggregate  (cost=0.00..18542321.56 rows=11 width=48)
  ->  Seq Scan on user_actions  (cost=0.00..15234876.23 rows=9823456 width=20)
        Filter: ((gender = '男'::bpchar) AND (age >= 25) AND (age <= 35) AND (action_date >= '2025-01-01'::date))

PostgreSQL 18(跳过扫描):

GroupAggregate  (cost=0.43..2341567.89 rows=11 width=48)
  ->  Index Skip Scan on user_actions using idx_city_gender_age_date  (cost=0.43..1987234.56 rows=123456 width=20)
        Index Cond: ((gender = '男'::bpchar) AND (age >= 25) AND (age <= 35) AND (action_date >= '2025-01-01'::date))

性能对比

版本执行方式执行时间Buffer 读取
PostgreSQL 17全表扫描45 秒8.2GB
PostgreSQL 18跳过扫描3.2 秒245MB

提升幅度:执行时间减少 93%,I/O 减少 97%

3.4 适用条件

跳过扫描并非万能,需要满足以下条件:

  1. 前导列基数适中

    • 基数过高(如主键):跳过扫描代价太大,不如全表扫描
    • 基数过低(如性别):收益不明显
    • 最佳范围:10-1000 个不同值
  2. 后续列过滤性强

    • 后续列的条件必须能有效过滤数据
    • 如果 gender = '男' 匹配 50% 数据,跳过扫描意义不大
  3. 索引列顺序合理

    • 仍然建议将高基数列放在前面
    • 跳过扫描是"保底方案",不应滥用

3.5 最佳实践

-- 场景:电商订单表
-- 查询模式:
-- 1. 按城市 + 状态 + 时间查询(80%)
-- 2. 按状态 + 时间查询(15%)
-- 3. 按时间查询(5%)

-- 传统方案:创建多个索引
CREATE INDEX idx_city_status_time ON orders (city, status, created_at);
CREATE INDEX idx_status_time ON orders (status, created_at);
CREATE INDEX idx_time ON orders (created_at);

-- PostgreSQL 18 方案:一个索引搞定
-- 利用跳过扫描支持查询 2 和 3
CREATE INDEX idx_city_status_time ON orders (city, status, created_at);

-- 查询 2 自动使用跳过扫描
SELECT * FROM orders 
WHERE status = 'pending' AND created_at >= '2025-08-01';

-- 查询 3 自动使用跳过扫描
SELECT * FROM orders 
WHERE created_at >= '2025-08-01';

四、虚拟生成列:用计算换存储

4.1 问题:存储型生成列的开销

PostgreSQL 12 引入了生成列(Generated Columns),但默认是 STORED 类型:

-- PostgreSQL 12-17
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    price NUMERIC(10, 2),
    quantity INTEGER,
    -- 存储型生成列:值被物理存储
    total_price NUMERIC(12, 2) GENERATED ALWAYS AS (price * quantity) STORED
);

问题

  1. 写入开销:每次插入或更新,都要计算并存储生成列
  2. 存储空间:生成列占用额外磁盘空间
  3. 修改困难:修改生成列表达式需要重写整个表

4.2 解决方案:虚拟生成列

PostgreSQL 18 将默认行为改为 VIRTUAL

-- PostgreSQL 18
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    price NUMERIC(10, 2),
    quantity INTEGER,
    -- 虚拟生成列:查询时计算,不存储
    total_price NUMERIC(12, 2) GENERATED ALWAYS AS (price * quantity) VIRTUAL
);

工作原理

  • 插入数据时,不计算 total_price
  • 查询 total_price 时,实时计算 price * quantity
  • 就像 Excel 中的公式单元格

4.3 实战对比

-- 创建测试表(1000 万行)
CREATE TABLE order_items (
    id BIGSERIAL PRIMARY KEY,
    order_id INTEGER,
    product_name VARCHAR(100),
    unit_price NUMERIC(10, 2),
    quantity INTEGER,
    discount_rate NUMERIC(4, 3) DEFAULT 0,
    -- 虚拟生成列
    subtotal NUMERIC(12, 2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL,
    discount_amount NUMERIC(10, 2) GENERATED ALWAYS AS (unit_price * quantity * discount_rate) VIRTUAL,
    final_price NUMERIC(12, 2) GENERATED ALWAYS AS (unit_price * quantity * (1 - discount_rate)) VIRTUAL
);

-- 插入数据
INSERT INTO order_items (order_id, product_name, unit_price, quantity, discount_rate)
SELECT 
    (random() * 100000)::INTEGER,
    'Product ' || (random() * 1000)::INTEGER,
    (random() * 1000)::NUMERIC(10, 2),
    (random() * 10 + 1)::INTEGER,
    CASE WHEN random() > 0.8 THEN (random() * 0.3)::NUMERIC(4, 3) ELSE 0 END
FROM generate_series(1, 10000000);

-- 查询测试
EXPLAIN (ANALYZE) 
SELECT 
    order_id,
    product_name,
    unit_price,
    quantity,
    subtotal,
    discount_amount,
    final_price
FROM order_items
WHERE final_price > 500
LIMIT 100;

存储空间对比

表结构表大小索引大小(id + order_id)
STORED 生成列1.2 GB215 MB
VIRTUAL 生成列892 MB215 MB

存储节省26%

写入性能对比

-- 批量插入 100 万行
INSERT INTO order_items (order_id, product_name, unit_price, quantity, discount_rate)
SELECT 
    (random() * 100000)::INTEGER,
    'Product ' || (random() * 1000)::INTEGER,
    (random() * 1000)::NUMERIC(10, 2),
    (random() * 10 + 1)::INTEGER,
    CASE WHEN random() > 0.8 THEN (random() * 0.3)::NUMERIC(4, 3) ELSE 0 END
FROM generate_series(1, 1000000);
表结构插入时间
STORED 生成列18.3 秒
VIRTUAL 生成列12.7 秒

写入提速31%

4.4 在虚拟生成列上创建索引

虚拟生成列不存储,但可以在其上创建索引:

-- 在虚拟生成列上创建索引
CREATE INDEX idx_final_price ON order_items (final_price);

-- 等价于函数索引
CREATE INDEX idx_final_price_func ON order_items ((unit_price * quantity * (1 - discount_rate)));

查询性能

-- 使用索引
EXPLAIN (ANALYZE) 
SELECT * FROM order_items WHERE final_price > 500 LIMIT 100;
Index Scan using idx_final_price on order_items  (cost=0.43..8.46 rows=100 width=80)
  Index Cond: ((unit_price * quantity * (1 - discount_rate)) > '500'::numeric)
Execution Time: 0.234 ms

4.5 何时选择 STORED vs VIRTUAL

场景推荐类型原因
计算简单、查询频繁VIRTUAL节省存储,计算成本低
计算复杂、查询频繁STORED避免重复计算
写入远多于查询VIRTUAL减少写入开销
需要在生成列上建索引两者皆可都支持索引
查询需要排序/分组STORED计算结果可被物化利用

五、OAuth 2.0 认证:融入现代身份体系

5.1 问题:数据库认证的孤岛

在云原生架构中,应用的身份认证通常由专业的身份提供商(IdP)管理:

  • Keycloak
  • Auth0
  • AWS Cognito
  • Azure AD
  • Okta

但数据库的认证仍然是独立的:每个用户一个密码,或者依赖操作系统的用户体系。这导致了两个问题:

  1. 权限管理割裂:用户离职后,需要在多个系统撤销权限
  2. 审计困难:无法统一追踪用户的数据库访问行为

5.2 解决方案:OAuth 2.0 认证

PostgreSQL 18 原生支持 OAuth 2.0 认证。用户可以使用 IdP 颁发的 Access Token 连接数据库。

认证流程

1. 用户从 IdP 获取 Access Token(如 Keycloak)
2. 用户连接 PostgreSQL,传递 Token
3. PostgreSQL 调用验证器检查 Token
4. 验证通过后,映射到数据库角色

5.3 实战配置

步骤 1:配置验证器

postgresql.conf 中:

# 加载 OAuth 验证器库
oauth_validator_libraries = '/usr/local/lib/pg_oauth_validator.so'

# OAuth 配置
oauth_issuer = 'https://keycloak.example.com/realms/myrealm'
oauth_audience = 'postgres-cluster'
oauth_jwks_uri = 'https://keycloak.example.com/realms/myrealm/.well-known/jwks.json'
oauth_claim_mappings = 'role:postgres_role,tenant:tenant_id'

步骤 2:配置 pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    all             all             10.0.0.0/8              oauth
host    all             all             ::1/128                 oauth

步骤 3:创建角色映射

-- 创建数据库角色
CREATE ROLE app_readonly;
CREATE ROLE app_readwrite;
CREATE ROLE app_admin;

-- 授权
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_readwrite;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_admin;

-- 配置 IdP 角色(在 Keycloak 中)
-- postgres_role = "app_readonly" / "app_readwrite" / "app_admin"

步骤 4:客户端连接

import psycopg2
import requests

# 从 Keycloak 获取 Token
token_response = requests.post(
    'https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token',
    data={
        'grant_type': 'password',
        'client_id': 'postgres-client',
        'username': 'user@example.com',
        'password': 'user_password'
    }
)
access_token = token_response.json()['access_token']

# 连接 PostgreSQL
conn = psycopg2.connect(
    host='postgres.example.com',
    database='mydb',
    user='oauth',  # 占位符,实际角色由 Token 决定
    password=access_token  # Token 作为密码传递
)

5.4 高级特性:Claims 映射

Token 中的 Claims 可以映射到数据库属性:

# postgresql.conf
oauth_claim_mappings = 'role:postgres_role,tenant:tenant_id,department:department_code'
-- 基于租户的行级安全(RLS)
CREATE POLICY tenant_isolation ON orders
    USING (tenant_id = current_setting('app.tenant_id'));

-- 触发器:从 Token 提取租户 ID
CREATE OR REPLACE FUNCTION set_tenant_from_token()
RETURNS VOID AS $$
BEGIN
    -- 假设 OAuth 模块提供了提取函数
    PERFORM set_config('app.tenant_id', oauth_get_claim('tenant_id'), false);
END;
$$ LANGUAGE plpgsql;

-- 登录时触发
CREATE EVENT TRIGGER on_oauth_login
ON oauth_login_success
EXECUTE FUNCTION set_tenant_from_token();

5.5 安全优势

传统密码认证OAuth 2.0 认证
密码可能泄露Token 有过期时间,可即时撤销
每个用户一个密码统一 IdP 管理
无法细粒度控制可基于 Claims 动态授权
审计困难完整审计链:IdP → PostgreSQL

六、UUIDv7 与时态约束:为现代应用设计的数据基石

6.1 UUIDv7:时间有序的 UUID

PostgreSQL 18 引入了 uuidv7() 函数:

SELECT uuidv7();
-- 结果示例:018b8a5a-7c3b-7d2e-8000-0a1b2c3d4e5f

UUIDv7 结构

| 48 bits 时间戳 | 4 bits 版本 | 12 bits 随机 | 2 bits 变体 | 62 bits 随机 |

对比 UUIDv4

特性UUIDv4UUIDv7
时间有序
索引友好❌(随机插入,页分裂多)✅(顺序插入,页分裂少)
可排序✅(前 48 bits 是时间戳)
碰撞概率极低极低

实战案例

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

-- 插入测试数据
INSERT INTO events (event_type, payload)
SELECT 
    (ARRAY['click', 'view', 'purchase', 'logout'])[(random() * 4)::INTEGER],
    jsonb_build_object('user_id', (random() * 10000)::INTEGER, 'page', '/page/' || (random() * 100)::INTEGER)
FROM generate_series(1, 1000000);

-- 测试索引性能
CREATE INDEX idx_events_id ON events (id);

EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM events WHERE id > '018b8a5a-0000-0000-0000-000000000000' LIMIT 100;

索引大小对比

主键类型索引大小(100 万行)页分裂次数
UUIDv422 MB12456
UUIDv722 MB234

性能提升:页分裂减少 98%,写入吞吐提升 40%

6.2 时态约束:防止时间旅行

PostgreSQL 18 引入了时态约束(Temporal Constraints):

-- 创建带时态约束的表
CREATE TABLE subscriptions (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    plan VARCHAR(50),
    valid_from DATE NOT NULL,
    valid_until DATE NOT NULL,
    -- 时态约束:valid_from < valid_until
    CONSTRAINT valid_period CHECK (valid_from < valid_until)
);

-- 插入测试
INSERT INTO subscriptions (user_id, plan, valid_from, valid_until)
VALUES (1, 'pro', '2025-08-01', '2025-09-01');  -- 成功

INSERT INTO subscriptions (user_id, plan, valid_from, valid_until)
VALUES (1, 'pro', '2025-09-01', '2025-08-01');  -- 失败:违反时态约束

更高级:防止重叠

-- 创建排除约束:同一用户不能有重叠的订阅
CREATE TABLE subscriptions (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    plan VARCHAR(50),
    valid_from DATE NOT NULL,
    valid_until DATE NOT NULL,
    -- 排除约束:同一 user_id 的时间段不能重叠
    CONSTRAINT no_overlap EXCLUDE USING gist (
        user_id WITH =,
        daterange(valid_from, valid_until) WITH &&
    )
);

-- 测试
INSERT INTO subscriptions (user_id, plan, valid_from, valid_until)
VALUES (1, 'pro', '2025-08-01', '2025-09-01');  -- 成功

INSERT INTO subscriptions (user_id, plan, valid_from, valid_until)
VALUES (1, 'premium', '2025-08-15', '2025-09-15');  -- 失败:与现有订阅重叠

七、升级实战:从 PostgreSQL 17 到 18

7.1 升级前检查

# 1. 检查版本
pg_config --version

# 2. 检查扩展兼容性
SELECT * FROM pg_available_extensions;

# 3. 备份数据
pg_dumpall -U postgres > backup_$(date +%Y%m%d).sql

# 4. 检查配置文件
cat postgresql.conf | grep -E '^(shared_buffers|work_mem|effective_cache_size)'

7.2 升级方式

方式一:pg_upgrade(推荐)

# 停止旧版本
systemctl stop postgresql@17-main

# 初始化新版本数据目录
/usr/lib/postgresql/18/bin/initdb -D /var/lib/postgresql/18/main

# 执行升级
/usr/lib/postgresql/18/bin/pg_upgrade \
    --old-bindir /usr/lib/postgresql/17/bin \
    --new-bindir /usr/lib/postgresql/18/bin \
    --old-datadir /var/lib/postgresql/17/main \
    --new-datadir /var/lib/postgresql/18/main \
    --check  # 先检查,成功后去掉 --check 执行实际升级

方式二:逻辑复制

# 在旧版本配置逻辑复制
cat >> postgresql.conf << EOF
wal_level = logical
max_replication_slots = 10
EOF

# 创建发布
CREATE PUBLICATION pg18_migration FOR ALL TABLES;

# 在新版本订阅
CREATE SUBSCRIPTION pg18_sub
    CONNECTION 'host=old_pg user=replicator password=xxx'
    PUBLICATION pg18_migration;

# 等待同步完成
# 切换应用连接到新版本

7.3 升级后优化

-- 1. 更新统计信息
ANALYZE VERBOSE;

-- 2. 重建关键索引(利用新特性)
REINDEX INDEX CONCURRENTLY idx_critical;

-- 3. 启用异步 I/O
ALTER SYSTEM SET io_method = 'aio';
ALTER SYSTEM SET effective_io_concurrency = 32;
SELECT pg_reload_conf();

-- 4. 检查跳过扫描是否生效
EXPLAIN (ANALYZE) SELECT * FROM your_table WHERE non_leading_column = 'value';

-- 5. 考虑将生成列改为虚拟型
ALTER TABLE your_table 
    ALTER COLUMN generated_col SET (generated = VIRTUAL);

八、性能优化最佳实践

8.1 配置优化清单

# postgresql.conf 关键参数

# 内存配置
shared_buffers = '4GB'                  # 系统内存的 25%
work_mem = '256MB'                      # 单个查询操作可用内存
effective_cache_size = '12GB'           # 系统可用缓存大小
maintenance_work_mem = '1GB'            # VACUUM、CREATE INDEX 可用内存

# I/O 配置(PostgreSQL 18 新特性)
io_method = 'aio'                       # 启用异步 I/O
effective_io_concurrency = 32           # NVMe SSD 建议 32-64

# 并行查询
max_parallel_workers_per_gather = 4     # 单个查询的最大并行 worker
max_parallel_workers = 8                # 全局最大并行 worker

# WAL 配置
wal_buffers = '64MB'
checkpoint_completion_target = 0.9
max_wal_size = '2GB'
min_wal_size = '1GB'

# 统计信息
default_statistics_target = 100         # 统计精度
track_activities = on
track_counts = on
track_io_timing = on                    # 启用 I/O 时间跟踪

8.2 监控关键指标

-- 1. 查看异步 I/O 效果
SELECT 
    relname,
    seq_scan,
    seq_tup_read,
    idx_scan,
    idx_tup_fetch,
    heap_blks_read,
    heap_blks_hit
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_tup_read DESC
LIMIT 10;

-- 2. 查看索引使用情况
SELECT 
    schemaname,
    relname,
    indexrelname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC
LIMIT 20;

-- 3. 查看跳过扫描使用情况(需要开启 explain analyze)
-- 通过 EXPLAIN 输出中的 "Index Skip Scan" 判断

-- 4. 查看虚拟生成列计算开销
SELECT 
    schemaname,
    relname,
    attname,
    (pg_stats).null_frac,
    (pg_stats).avg_width
FROM pg_stats
WHERE attname IN (
    SELECT attname 
    FROM pg_attribute 
    WHERE attgenerated = 'v'  -- 虚拟生成列
);

九、踩坑清单

9.1 异步 I/O 相关

问题症状解决方案
操作系统不支持 io_uring启动报错升级 Linux 内核到 5.1+
effective_io_concurrency 过高内存耗尽根据硬件调整,通常不超过 64
云存储延迟高AIO 收益不明显云存储建议 io_method = 'sync' 或降级 concurrency

9.2 跳过扫描相关

问题症状解决方案
前导列基数过高跳过扫描比全表扫描慢检查 EXPLAIN,考虑创建专用索引
统计信息不准优化器不选择跳过扫描执行 ANALYZE 更新统计
索引列过多索引过大控制索引列数量,通常不超过 4 列

9.3 虚拟生成列相关

问题症状解决方案
计算表达式复杂查询变慢改为 STORED 类型或预计算
在虚拟列上排序大排序开销创建索引或使用 STORED
表达式依赖外部数据计算失败确保表达式自包含

9.4 OAuth 认证相关

问题症状解决方案
Token 过期时间过短频繁重连设置合理的 Token 过期时间(如 1 小时)
JWKS 端点不可达认证失败配置本地缓存或高可用 IdP
Claims 映射错误权限异常检查 oauth_claim_mappings 配置

十、总结与展望

PostgreSQL 18 是一次里程碑式的发布,标志着 PostgreSQL 从"功能丰富"走向"性能卓越"。五大核心特性:

  1. 异步 I/O:让数据库充分利用现代存储硬件
  2. 跳过扫描:打破多列索引的前导列限制
  3. 虚拟生成列:用计算换存储,灵活高效
  4. OAuth 2.0:融入现代身份体系
  5. UUIDv7 & 时态约束:为现代应用设计的数据基石

适用场景

  • 数据仓库、分析型负载 → 异步 I/O 收益最大
  • 查询模式多变的应用 → 跳过扫描减少索引数量
  • 写入密集型应用 → 虚拟生成列降低写入开销
  • 云原生架构 → OAuth 2.0 简化权限管理

升级建议

  • 开发环境:立即升级,体验新特性
  • 生产环境:先在测试环境验证,使用 pg_upgrade --check 检查兼容性
  • 关注点:异步 I/O 需要操作系统支持,跳过扫描依赖统计信息准确性

PostgreSQL 的演进从未停止。从 1986 年的 Berkeley 项目,到今天的 PostgreSQL 18,它始终在解决实际问题。这不是一次炫技式的升级,而是一次务实的性能优化。对于正在管理大规模数据的团队,PostgreSQL 18 值得你花时间深入了解。


参考资料

  1. PostgreSQL 18 Release Notes: https://www.postgresql.org/docs/18/release-18.html
  2. PostgreSQL Asynchronous I/O Implementation: src/backend/storage/aio/
  3. Skip Scan Optimization: src/backend/access/index/indexskipscan.c
  4. Generated Columns Design: src/backend/commands/tablecmds.c
  5. OAuth 2.0 RFC 6749: https://datatracker.ietf.org/doc/html/rfc6749
  6. UUIDv7 Draft: https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format

推荐文章

Vue中如何使用API发送异步请求?
2024-11-19 10:04:27 +0800 CST
Go 并发利器 WaitGroup
2024-11-19 02:51:18 +0800 CST
Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
程序员茄子在线接单