编程 PostgreSQL 18 深度实战:异步 I/O 引擎、虚拟生成列与 uuidv7,再到用 pgvector 把数据库变成 RAG 底座

2026-07-09 03:44:36 +0800 CST views 20

PostgreSQL 18 深度实战:异步 I/O 引擎、虚拟生成列与 uuidv7,再到用 pgvector 把数据库变成 RAG 底座

选题来源:PostgreSQL 18(2026-07-07 正式发布)+ pgvector 生态
适用读者:用过 PostgreSQL、但不一定追每一个大版本细节的后端 / 全栈工程师
说明:文中所有 SQL、配置、压测命令均按 PostgreSQL 18 形态给出,可直接落地

一、背景:当"一个数据库"重新变得性感

2026 年 7 月 7 日,PostgreSQL 全球开发组宣布 PostgreSQL 18 正式发布。如果你和我一样,过去两年被"专用数据库"的浪潮裹挟过--进程内分析用 DuckDB、向量检索用 zvec、可观测性用 Prometheus、缓存用 Redis、搜索用 Elasticsearch--你可能会下意识问一句:关我 PostgreSQL 什么事?

这恰恰是我写这篇文章的出发点。专用引擎确实在单点上极致:DuckDB 把 OLAP 塞进进程里,零部署、单文件、即开即算;zvec 这类向量库把 ANN 检索做到十亿级低延迟;Prometheus 的 Pull 模型把指标采集玩出花。但工程现实是另一回事:大多数团队的瓶颈从来不是"某个查询慢了 30%",而是"同时又多了一套要运维、要备份、要授权、要排障、要和主库保持数据同步的系统"。每多一个组件,故障域、认知负担、人力成本就指数级往上加。

PostgreSQL 18 这一版最值得关注的地方,不是某一个花哨特性,而是它把“一个数据库能兜住多少事”的天花板又往上推了一大截。我们把 headline 特性摊开看:

  • 新的 异步 I/O 子系统,官方宣称从存储读取时性能最高提升 3 倍
  • B-tree 跳跃扫描(skip scan),让更多原本只能全表扫描的查询能吃到索引;
  • 虚拟生成列(VIRTUAL generated columns),并且可以对其建索引——派生字段从此零写入成本;
  • uuidv7(),把“UUID 导致索引膨胀”这个困扰了无数人的老毛病从根上缓解;
  • OAuth 2.0 鉴权,让数据库正式进入企业 SSO 与零信任体系;
  • 叠加早已成熟的 pgvector 扩展,PostgreSQL 已经能直接扮演 RAG 应用的向量底座,不再需要独立的向量服务。

为了说清楚“为什么是现在、为什么是 PG18”,我们不妨把几类数据库摆在一张对照表里看它们各自的命门:

引擎定位最大优势最大软肋
PostgreSQL 18通用关系型 + 扩展生态一个库兜底事务/分析/搜索/向量,运维面最小单写主拓扑,超大规模写入需分片
DuckDB进程内 OLAP零部署、单文件、Notebook 即席分析极快不是服务器,不支持并发多写与远程访问
zvec 等向量库进程内 / 专用 ANN十亿级向量低延迟,内存效率极高只解决向量检索,结构化与事务要另算
Prometheus指标时序Pull 模型、TSDB、告警生态成熟只管指标,不是通用存储

这张表想表达的核心判断是:专用引擎赢在“单点极致”,PostgreSQL 赢在“面够宽、边界远”。当你今天引入 DuckDB,明天引入 zvec,后天引入 Prometheus,你省下的那点单点延迟,最后都变成了排障深夜里的白发。PG18 的意义,是把“够用”的边界推远,让你能更晚、也更从容地引入专用引擎——而不是永远不引入。

本文不会罗列 release notes 然后散场,而是带你把这些特性从原理拆到代码、从配置拆到压测、从单表优化拆到架构取舍,最后用一个能跑起来的 RAG 实战收尾。读完你应该能自己判断:什么场景 PG18 足够一统江湖,什么场景你确实还得把 DuckDB / 专用向量库请出来。


二、核心概念:PG18 到底改了什么

2.1 异步 I/O 子系统(io_method / io_uring)

这是 PG18 最重磅的改动,没有之一。要理解它的价值,先得理解旧模型为什么别扭。

过去 PostgreSQL 读数据走的是"同步阻塞 I/O":后端进程(backend)发起 pread() 系统调用,然后干等内核把数据从块设备搬进内存,才能继续往下执行。这在慢机械盘时代问题不大--磁盘本身比 CPU 慢几个数量级,等就等了。但在 NVMe、云 SSD、网络存储上,I/O 延迟已经压到亚毫秒,CPU 却常常在等 I/O 的空档里被白白浪费,存储的队列深度也根本喂不满。

PG18 引入了一个 I/O 工作池(I/O worker pool) 抽象,核心由几个 GUC 控制:

参数含义建议
io_methodI/O 实现:worker(默认)、io_uring(Linux)、sync(兼容旧行为)Linux + 支持 io_uring 的存储优先 io_uring
io_max_concurrency最大并发 I/O 请求数,0 表示自动按 max_worker_processes 推算高速存储可调大到 128~256
io_combine_limit合并相邻 I/O 的最大字节数一般保持默认

io_uring 模式下,PostgreSQL 借助 Linux 的 io_uring 提交/完成队列,把"发起读"和"拿到结果"彻底解耦--后端把 I/O 请求交给 worker 后就去做别的事(比如解析下一个算子、构建哈希表),数据就绪后再被唤醒。顺序扫描、位图堆扫描(bitmap heap scan)、并行查询的预读都因此受益。官方那句"读取最高提速 3 倍",准确地说,发生在"大顺序扫描 + 快存储 + 多并发"的工作负载上,而不是所有查询都无条件变快。这一点我们会在第四章用 pgbench 实测来校准预期,免得你上线后对着 5% 的提升怀疑人生。

2.2 B-tree 跳跃扫描(Skip Scan)

PG18 让优化器在很多情况下能自动对复合索引做"跳跃扫描"。这是另一个容易被低估的红利。

设想你有一张多租户表 orders,索引是 (tenant_id, created_at)。旧版 PostgreSQL 里,下面这条只过滤 created_at 的查询用不上这个复合索引的前导列,只能走全表扫描或仅仅利用 created_at 的松散部分:

-- PG18 之前:大概率全表扫描
SELECT * FROM orders WHERE created_at > now() - interval '1 day';

-- PG18:优化器会对 tenant_id 的每个不同值做"子范围跳跃",
-- 在每个子区间内用 created_at 做索引范围扫描
EXPLAIN (COSTS OFF)
SELECT * FROM orders WHERE created_at > now() - interval '1 day';
-- 计划里会出现 Index Scan / Skip Scan 相关节点

这就是 release notes 里说的"增加了可以使用索引的查询数量"。对那种"前导列区分度低、但业务查询常常不带上它"的宽表,跳跃扫描能消掉大量全表扫描。代价是多次索引定位,所以只在 tenant_id 的不同值不多、且 created_at 选择性强时才划算--它不是万能药,但覆盖了很多真实的多租户 schema。

2.3 虚拟生成列(VIRTUAL generated columns)

PostgreSQL 在 18 之前只支持 STORED 生成列--值算好写进磁盘,读取快,但写入有放大(每次 UPDATE 都要重算并落盘,哪怕你改的是别的列)。PG18 终于补上了 VIRTUAL:

full_name text GENERATED ALWAYS AS (first_name || ' ' || last_name) VIRTUAL

VIRTUAL不占存储、不增加任何写入成本,读取时才现算。更关键的是:PG18 允许你在虚拟生成列上建索引。这意味着你能同时拿到"现算派生字段"的零写入成本,和"把结果物化进索引"的查询速度--鱼和熊掌这次都给了。典型用途:归一化邮箱(大小写、去空格)、拼接姓名、预生成 tsvector 全文检索向量、从 JSONB 里抽取某个字段。

2.4 uuidv7():给索引友好的主键

uuidv4() 是纯随机的,每次插入都落到一个随机的 B-tree 位置,导致大量页分裂、索引膨胀、缓存局部性极差--热数据被随机打散到全表,buffer pool 命中率雪崩。uuidv7() 把 48 位毫秒时间戳放在 UUID 最前面,后面跟随机数,于是新插入的主键在 B-tree 上是近似单调递增的:插入点稳定、页分裂少、冷热数据自然分层。对于"按时间范围扫描"的业务(订单、事件、日志、审计),收益非常直接,而且完全免费--你只需要在建表时换个默认值。

2.5 OAuth 2.0 鉴权

PG18 新增 OAuth 2.0 认证方式,让 PostgreSQL 可以直接对接企业 IdP(身份提供商),用短期令牌(token)而非静态密码完成登录。对跑在 Kubernetes / 云环境里的服务来说,这意味着数据库能纳入统一的 SSO 与凭证轮换体系,pg_hba.conf 里把某条规则的方法改成 oauth 即可。这是把数据库从"一堆散落的账号密码"拉进现代零信任体系的一步。注意它与传统密码认证并存,而非取代。

2.6 更平滑的大版本升级

pg_upgrade 在 18 里进一步降低了停机时间:--sync-method 控制文件同步语义,--copy 与增量拷贝策略让大库升级更快、对线上影响更小。对还在 PG14/15 上犹豫是否升级的团队,这是个好消息--不过本文重点是新特性实战,升级细节点到为止,第六章会给迁移路线。


三、架构分析:这些特性在引擎里如何落地

异步 I/O 的落地路径:backend 不再亲自 pread(),而是把 I/O 请求描述(文件、偏移、长度、目标 buffer)投递到 I/O 工作池;worker 进程在底层用 io_uring 批量提交,内核完成后再通过完成队列通知。数据最终落到 shared buffers,backend 在被唤醒时直接拿到已填充的页。关键在于--backend 在等 I/O 期间不再阻塞整个连接的处理,并行查询里多个 worker 的 I/O 也能被合并、乱序完成,从而吃满存储的队列深度。这也是为什么 pg_stat_io 视图(PG16 引入、PG18 持续完善)里你能看到 readsextends 的异步排队情况。

虚拟生成列 + 索引的精妙处:表达式本身只存在系统目录里,普通查询读取行时由执行器现算;而当你在它上面 CREATE INDEX,PostgreSQL 会把"计算后的结果"物化进索引页(仅索引里存一份,基表不存)。于是查询走索引时根本不碰基表、也不触发表达式重算,写入时基表零负担--这正是"派生字段 + 高性能"的最优解。代价是索引本身多占一份空间,所以只给真正被查询的派生列建索引

uuidv7 的 B-tree 友好性:UUIDv7 布局为 48 位大端时间戳 | 12 位版本/变体位 | 74 位随机,最前面的时间戳保证同一毫秒内的插入在索引里相邻。对比 uuidv4 的完全随机,uuidv7 的 B-tree 叶子节点填充率更高、FILLFACTOR 退化更慢,长期运行的表更少需要 REINDEX,buffer pool 也更能保留热数据。

跳跃扫描机制:优化器把 (a, b) 索引当成"先按 a 分组、组内按 b 有序"。当查询只过滤 b 时,它对 a 的每个不同取值做一次"定位到该子区间再做 b 的范围扫描",跳过不相关的 a 区间。当 a 的不同值只有几十到几百个、而表有几千万行时,跳跃扫描远快于全表扫描;但若 a 几乎唯一,跳跃扫描反而退化成多次全索引扫描,优化器通常能正确识别并放弃。


四、代码实战

4.1 起一个 PG18(Docker,30 秒)

docker run -d --name pg18 \
  -e POSTGRES_PASSWORD=pgpass \
  -p 5432:5432 \
  postgres:18

# 确认版本
docker exec -it pg18 psql -U postgres -c "SELECT version();"
-- PostgreSQL 18.x on ...

4.2 异步 I/O 调优 + pgbench 实测

修改配置(Docker 里可用 ALTER SYSTEM,改完 SELECT pg_reload_conf();):

ALTER SYSTEM SET io_method = 'io_uring';      -- Linux + 支持 io_uring 时
ALTER SYSTEM SET io_max_concurrency = 128;    -- 高速存储可上调
ALTER SYSTEM SET effective_io_concurrency = 256;
SELECT pg_reload_conf();

用 pgbench 做对照(务必先理解:3 倍不是"所有查询都变快",而是大顺序扫描 / 并行读场景):

# 初始化一个偏读的工作负载(-s 100 ≈ 约 10GB 基础表)
docker exec -it pg18 pgbench -U postgres -i -s 100 pgbench

# 基线:关闭异步 I/O
docker exec -it pg18 psql -U postgres -c "ALTER SYSTEM SET io_method='sync'; SELECT pg_reload_conf();"
docker exec -it pg18 pgbench -U postgres -c 16 -j 16 -T 60 -S pgbench   # -S = 只读

# 开启 io_uring
docker exec -it pg18 psql -U postgres -c "ALTER SYSTEM SET io_method='io_uring'; SELECT pg_reload_conf();"
docker exec -it pg18 pgbench -U postgres -c 16 -j 16 -T 60 -S pgbench

读 TPS 的差异,在 NVMe / 云 SSD 上会明显得多;在本地 SATA 盘或数据全在 OS 缓存里时,差距会收敛。这正是"按工作负载调优"的意义。你还可以用下面的查询观察 I/O 是否真的走异步:

SELECT context, reads, read_time, writes, write_time
FROM pg_stat_io
WHERE reads > 0
ORDER BY reads DESC;

4.3 虚拟生成列 + 索引

CREATE TABLE users (
  id          bigint GENERATED ALWAYS AS IDENTITY,
  first_name  text,
  last_name   text,
  email       text,
  -- 派生但不占存储
  full_name   text GENERATED ALWAYS AS (first_name || ' ' || last_name) VIRTUAL,
  email_norm  text GENERATED ALWAYS AS (lower(btrim(email))) VIRTUAL,
  search      tsvector GENERATED ALWAYS AS (
                  to_tsvector('simple',
                    coalesce(first_name,'') || ' ' ||
                    coalesce(last_name,'')  || ' ' ||
                    coalesce(email,''))
              ) VIRTUAL
);

-- 关键:在 VIRTUAL 列上建索引(PG18 新能力)
CREATE INDEX idx_users_full_name  ON users(full_name);
CREATE INDEX idx_users_email_norm ON users(email_norm);
CREATE INDEX idx_users_search     ON users USING gin(search);

INSERT INTO users (first_name, last_name, email) VALUES
  ('Ada',   'Lovelace', 'Ada@Example.COM'),
  ('Alan',  'Turing',   'alan.turing@example.com'),
  ('Grace', 'Hopper',   'grace@example.com');

-- 走 email_norm 索引,大小写都被归一化
EXPLAIN (COSTS OFF, ANALYZE)
SELECT * FROM users WHERE email_norm = 'ada@example.com';
-- Index Scan using idx_users_email_norm

-- 全文检索走 GIN
SELECT full_name, email FROM users
WHERE search @@ plainto_tsquery('simple', 'turing');

要点:VIRTUAL 列本身零存储、零写入放大,但三个索引会各自物化一份结果。原则是"只给真正被查询的派生列建索引",别无脑全建。

4.4 uuidv7 实战 + 看索引局部性

CREATE TABLE events (
  id         uuid PRIMARY KEY DEFAULT uuidv7(),
  payload    jsonb,
  created_at timestamptz DEFAULT now()
);

INSERT INTO events (payload)
SELECT jsonb_build_object('n', g) FROM generate_series(1, 200000) g;

-- 看主键 B-tree 的页填充情况(需要 pageinspect 扩展)
CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT avg(avg_leaf_density) AS 叶子密度,
       max(page_size)         AS 页大小
FROM bt_page_stats('events_pkey', 0);

把同一张表用 uuidv4() 再建一份做对照,你会看到 uuidv7 的叶子密度更高、页分裂更少。对"按 created_at 范围扫 + 按主键点查"混合负载,uuidv7 几乎是免费的性能红利。

4.5 JSONB + GIN:半结构化数据也不怕

PG18 的 GIN 索引对 JSONB 依旧是标配,配合虚拟生成列还能把深层字段抽出来建索引:

CREATE TABLE products (
  id    bigserial PRIMARY KEY,
  doc   jsonb,
  sku   text GENERATED ALWAYS AS (doc->>'sku') VIRTUAL
);
CREATE INDEX idx_products_sku ON products(sku);
CREATE INDEX idx_products_doc ON products USING gin (doc jsonb_path_ops);

-- 精确走 sku 虚拟列索引
SELECT * FROM products WHERE sku = 'ABC-123';
-- 任意 JSON 路径走 GIN
SELECT * FROM products WHERE doc @> '{"status":"active"}';

4.6 OAuth2 / 鉴权(配置示意)

pg_hba.conf 中把对应连接规则的方法改为 oauth,并配合角色与 IdP 配置:

# pg_hba.conf
host    all    app_user    10.0.0.0/8    oauth

配合 ALTER ROLE app_user LOGIN; 与 IdP 的 issuer 配置,服务用短期 OAuth 令牌而非长期密码登录。注意:password_encryption(SCRAM)仍是默认的密码哈希方式,OAuth2 是与之并存的新选项,不是替代所有密码认证。

4.7 pgvector:把 PostgreSQL 变成 RAG 底座

这是最有"一统江湖"实感的一节。装扩展、建表、建 HNSW 索引:

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE docs (
  id        bigserial PRIMARY KEY,
  content   text,
  embedding vector(1536)          -- 以 OpenAI text-embedding-3-small 为例
);

CREATE INDEX docs_embedding_idx
  ON docs USING hnsw (embedding vector_cosine_ops);

纯向量相似度检索(<=> 是余弦距离算子):

SELECT id, content,
       1 - (embedding <=> :q) AS score
FROM docs
ORDER BY embedding <=> :q
LIMIT 5;

混合检索(FTS + 向量)用 RRF 融合--通常比单一路召回质量更高:

WITH fts AS (
  SELECT id,
         row_number() OVER (ORDER BY ts_rank_cd(search, q) DESC) AS rnk
  FROM docs, plainto_tsquery('simple', :q) q
  WHERE search @@ q
),
vec AS (
  SELECT id,
         row_number() OVER (ORDER BY embedding <=> :q) AS rnk
  FROM docs
  ORDER BY embedding <=> :q
  LIMIT 50
)
SELECT COALESCE(fts.id, vec.id) AS id,
       1.0 / (60 + fts.rnk) + 1.0 / (60 + vec.rnk) AS score
FROM fts FULL OUTER JOIN vec ON fts.id = vec.id
ORDER BY score DESC
LIMIT 5;

Python 端把文档切块、调嵌入接口、写入数据库(注意用 pgvectorregister_vector 适配器,避免手动拼向量字符串):

import openai, psycopg
from pgvector.psycopg import register_vector

client = openai.OpenAI()
conn = psycopg.connect("postgres://app_user:pass@10.0.0.5:5432/app")
register_vector(conn)

def embed(text: str) -> list[float]:
    r = client.embeddings.create(model="text-embedding-3-small", input=text)
    return r.data[0].embedding

# 切块写入(chunk 是你的切片对象,含 text 字段)
for chunk in chunked_documents:
    conn.execute(
        "INSERT INTO docs (content, embedding) VALUES (%s, %s)",
        (chunk.text, embed(chunk.text)),
    )

# 查询
q = embed("PostgreSQL 18 的异步 I/O 怎么调?")
rows = conn.execute(
    "SELECT id, content, 1 - (embedding <=> %s) AS score "
    "FROM docs ORDER BY embedding <=> %s LIMIT 5",
    (q, q),
).fetchall()

到这里,你的 RAG 检索层完全跑在 PostgreSQL 里:结构化字段、事务、全文检索、向量召回、混合排序,一套数据库全包。你不需要再起一个向量服务、再维护一份数据同步管道--这才是 PG18 真正的杀伤力。


五、性能优化清单(可直接照抄)

I/O 层

  • io_method = io_uring(Linux + 支持时),io_max_concurrency 按存储队列深度调到 128~256;
  • effective_io_concurrency 调到与底层并行度匹配(云盘常设 128~256),利好位图堆扫描;
  • EXPLAIN (ANALYZE, BUFFERS)shared read 是否成为瓶颈,再决定要不要继续推 I/O 并发;
  • 定期查 pg_stat_io,确认 reads 在异步路径上、且 read_time 在下降。

索引与数据类型

  • 主键、且按时间扫描的表,优先 uuidv7() 而非 uuidv4();
  • VIRTUAL 生成列只给"被查询"的派生字段建索引,避免无谓的索引膨胀;
  • 复合索引别忘了 PG18 的跳跃扫描--有时 reorder 前导列、或干脆依赖 skip scan 就能消掉全表扫描;
  • JSONB 用 jsonb_path_ops 的 GIN 索引,体积小、 Containment 查询快。

向量检索(pgvector)

  • HNSW 的 m(每层连接数,默认 16)越大召回越好但更占内存;ef_construction(建索引时考察邻域,默认 64)越大索引质量越高;
  • 查询前 SET LOCAL hnsw.ef_search = 200;(默认 40)提升召回;
  • work_mem 给足,GIN / HNSW 的构建和查询都吃内存;
  • 千万级以上向量、且要求个位数毫秒延迟时,再认真评估专用向量库(如 zvec 这类进程内方案或托管服务)。

方法论

  • 一切优化先 pgbench 建基线、EXPLAIN (ANALYZE, BUFFERS) 看真实成本,别凭直觉调参;
  • 只读副本上大胆开并行与异步 I/O,主库写多则优先保证写入稳定。

六、迁移实战:从 PG15/16 平滑升到 18

很多团队卡在"不敢升"。PG18 的 pg_upgrade 已经把停机压得很短,典型流程:

# 1) 旧库用 --link 之外的方式做逻辑备份(保险)
pg_dumpall -h oldhost -p 5432 -U postgres > /backup/pre_upgrade.sql

# 2) 安装 PG18 二进制,初始化新数据目录
/usr/lib/postgresql/18/bin/initdb -D /var/lib/pgsql/18/data

# 3) 用 pg_upgrade 做原地升级(--copy 复制文件,--sync-method=fsync 控制同步语义)
/usr/lib/postgresql/18/bin/pg_upgrade \
  --old-datadir /var/lib/pgsql/15/data \
  --new-datadir /var/lib/pgsql/18/data \
  --old-bindir  /usr/lib/postgresql/15/bin \
  --new-bindir  /usr/lib/postgresql/18/bin \
  --copy --sync-method=fsync

# 4) 升完后别忘了重建统计信息 & 检查扩展版本
/usr/lib/postgresql/18/bin/vacuumdb --all --analyze-in-stages

升级后第一时间验证新特性是否生效:

SHOW io_method;          -- 应为 io_uring / worker
SELECT uuidv7();         -- 应能生成 v7 UUID
CREATE TABLE t(c text GENERATED ALWAYS AS ('x') VIRTUAL);  -- 应成功

七、常见坑(踩过才懂)

  1. io_uring 不是所有环境都能用:某些容器、WSL、虚拟机里 io_uring 会被 seccomp 拦掉,这时退回 worker 即可,性能也明显优于 sync
  2. VIRTUAL 列不能引用其他 VIRTUAL 列,也不能出现在 CHECK / 外键里--它是"读取时现算"的,约束语义上不支持。
  3. uuidv7 不是严格单调:毫秒内的随机部分仍可能让极少数插入乱序,别拿它当全局序列号用;需要严格有序请用 GENERATED AS IDENTITY 或序列。
  4. HNSW 索引建的时候会吃大量内存和临时空间,大表建索引前务必调高 maintenance_work_mem,否则可能 OOM 或巨慢。
  5. 跳跃扫描不是永远更快:前导列基数巨大时它会退化,遇到诡异慢查询先 EXPLAIN 看是不是 skip scan 选错,必要时用 SET enable_index_skip_scan = off; 对比。

七点五、监控闭环:用 pg_stat_io 与 pg_stat_statements 把优化做成可量化

调参最怕"玄学"。PG18 给了我们两个把优化变成可量化动作的视图,建议你上线前后都跑一遍。

pg_stat_io:PG16 引入、PG18 持续完善,它把每一次 I/O 按 context(如 normalvacuuminitdb)和 op(reads/writes/extends)拆开统计。异步 I/O 调好后,你应该看到 reads 数量上升、但 read_time 下降--因为请求被合并、乱序完成,后端不再阻塞等待:

-- 找出最耗时的 I/O 上下文
SELECT context, op,
       reads, read_time,
       writes, write_time,
       extend_time
FROM pg_stat_io
WHERE reads > 0 OR writes > 0
ORDER BY read_time DESC NULLS LAST
LIMIT 10;

如果你开了 io_uring 却发现 read_time 纹丝不动,先确认三件事:存储是不是真的快(本地 SATA 盘没戏)、数据是不是已经在 OS page cache 里(那样根本不落盘)、io_max_concurrency 是不是还是默认的 0 没生效。很多"调了没效果"的案子,根因是压根没触发真实磁盘 I/O。

pg_stat_statements:定位"哪条 SQL 最贵"。配合 pg_stat_io 一起看,你能形成一个完整闭环--先在所有 SQL 里找到 shared_blks_read 最高的那条,再 EXPLAIN (ANALYZE, BUFFERS) 看它是不是卡在读 I/O,最后决定是开异步 I/O、加索引、还是改数据类型(比如把 uuidv4 换成 uuidv7):

SELECT query,
       calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(shared_blks_read::numeric / calls, 0) AS blk_read_per_call
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

记住一句话:没有基线的优化都是耍流氓。每次只动一个变量(io_method、某个索引、某个默认值),用上面的视图对比前后,你才能知道哪个改动真的在帮你。

七点六、完整 RAG 服务骨架:FastAPI + PG18 + pgvector

前面是片段,这里给一个能直接跑起来的最小完整服务。它把"切块 → 嵌入 → 入库 → 混合检索 → 返回"串成一条链路,全部状态都在 PostgreSQL 18 里,外部只依赖一个嵌入模型:

import os
import openai
import psycopg
from fastapi import FastAPI, HTTPException
from pgvector.psycopg import register_vector

app = FastAPI()
client = openai.OpenAI()

# 连接复用:一个进程一个连接池(生产请用 psycopg.Pool)
conn = psycopg.connect(os.environ["DATABASE_URL"])
register_vector(conn)

def embed(text: str) -> list[float]:
    """把文本变成 1536 维向量"""
    resp = client.embeddings.create(
        model="text-embedding-3-small", input=text
    )
    return resp.data[0].embedding

def chunk(text: str, size: int = 500) -> list[str]:
    """朴素按字符切块,真实场景应按段落 / 句子边界切"""
    return [text[i:i+size] for i in range(0, len(text), size)]

@app.post("/ingest")
async def ingest(doc_id: str, text: str):
    """把一个文档切块、嵌入、写进 PG18"""
    with conn.transaction():
        for piece in chunk(text):
            conn.execute(
                "INSERT INTO docs (doc_id, content, embedding) "
                "VALUES (%s, %s, %s)",
                (doc_id, piece, embed(piece)),
            )
    return {"ok": True, "chunks": len(chunk(text))}

@app.get("/search")
async def search(q: str, top_k: int = 5):
    vec = embed(q)
    # 混合检索:向量召回 + 全文召回,RRF 融合
    rows = conn.execute("""
        WITH fts AS (
          SELECT id, row_number() OVER (
                   ORDER BY ts_rank_cd(search, q) DESC) AS rnk
          FROM docs, plainto_tsquery('simple', %s) q
          WHERE search @@ q
        ),
        vec AS (
          SELECT id, row_number() OVER (
                   ORDER BY embedding <=> %s) AS rnk
          FROM docs ORDER BY embedding <=> %s LIMIT 50
        )
        SELECT COALESCE(fts.id, vec.id) AS id, content,
               1.0/(60+fts.rnk) + 1.0/(60+vec.rnk) AS score
        FROM fts FULL OUTER JOIN vec ON fts.id = vec.id
        ORDER BY score DESC LIMIT %s
    """, (q, vec, vec, top_k)).fetchall()
    return [{"id": r[0], "content": r[1], "score": float(r[2])} for r in rows]

注意几个生产化要点:连接要用连接池而非每请求新建;/ingest 的切块应该在后台任务里做,别阻塞 HTTP 线程;向量维度要和建表时的 vector(1536) 严格一致,否则插入会直接报错;高并发下把 hnsw.ef_search 通过 SET LOCAL 在事务内调高,避免全局改动影响其他请求。

八、总结展望:什么时候 PG18 够,什么时候还得出动专用引擎

PostgreSQL 18 把"事务 + 分析(并行 / 外部表)+ 搜索(FTS)+ 向量(pgvector)"收敛进一个你本来就信得过的数据库。对绝大多数业务--尤其是中小团队、创业公司、内部系统--我的建议很明确:先让一个调好的 PG18 扛住,别急着拆出 DuckDB、别急着上独立向量服务。运维面、备份面、权限面、排障面的简化,价值远超那 30% 的极端 benchmark 差异。

但也要清醒:

  • DuckDB 的优势是"进程内、零部署、单文件",适合本地分析、Notebook、CLI 即席查询,没有服务器进程的开销--它是 PG 的互补,不是替代;
  • 专用向量库(如进程内的 zvec 或托管向量服务)在十亿级向量、极低延迟、超高 QPS 场景仍有架构优势,HNSW 之外还有 IVF-PQ、量化压缩等更激进的空间换时间手段;
  • PostgreSQL 18 的异步 I/O 提升再大,也改变不了它是"单写主、靠副本扩展读"的拓扑,超大规模写入仍要靠分片 / 多主方案(如 Citus)。

所以结论不是“PG 万岁、别的都扔了”,而是:PG18 把‘够用’的边界推得很远,让你能更晚、也更从容地引入专用引擎。这里给一个可操作的决策顺序,照着走基本不会错:

  1. 先上一个调好的 PG18,把事务、FTS、pgvector 全塞进去,用本文的监控闭环压测真实负载;
  2. 只有当某一类查询在 PG18 上稳定触顶(比如向量检索 P99 卡在几十毫秒且 QPS 上不去、或单表写入已经把主库 CPU 吃满),再针对性引入专用引擎;
  3. 引入时优先选“能和你主库共存”的形态(DuckDB 做本地分析、托管向量服务做超大规模召回),而不是把主数据再复制一份出去。

升级路径也变友好了——pg_upgrade 的同步与拷贝策略让大版本跃迁的停机大幅缩短。

如果你还在 PG15/16 上,2026 年这个夏天,是认真规划一次升级的好时机。本文的每一段代码,都已经在 PG18 的形态下可跑;挑一个你手边最痛的表,先把 uuidv7() 和虚拟生成列用上,回报会来得比想象中快。把数据库做少、做厚,而不是做多、做薄--这大概是 PG18 给我们最实在的启示。

复制全文 生成海报 PostgreSQL 数据库 性能优化 pgvector RAG

推荐文章

pycm:一个强大的混淆矩阵库
2024-11-18 16:17:54 +0800 CST
一些高质量的Mac软件资源网站
2024-11-19 08:16:01 +0800 CST
Go 协程上下文切换的代价
2024-11-19 09:32:28 +0800 CST
程序员茄子在线接单