Skip to content
扩展与生态

扩展与生态

PostgreSQL 真正的护城河不是 SQL 实现得多全,而是扩展生态:一台 PG 装几个扩展,常常能干掉半套中间件——pgvector 顶一个向量库、pg_cron 顶一个轻量调度器、PostGIS 顶一个 GIS 服务、TimescaleDB 顶一个时序库。00 章开篇就点过这张牌。但先把冷水泼在前面:扩展是在数据库进程里跑第三方代码。它跟你的表共享同一块内存、同一个事务上下文、同一次崩溃半径——一个扩展的后台 worker 泄漏,OOM 的是整个 postmaster;一个 C 函数解引用空指针,断开的是当前 backend。选错扩展不是"少个功能",而是生产事故的来源。本章按"它解决什么问题 → 装它要付出什么运维成本 → 呼应本手册哪一章的原理"三条线,把主流扩展讲一遍。

版本号说明:以下扩展版本均为 2026-10-20 通过各自官方仓库(GitHub tag / release feed)实测核验;PostGIS 取自官方 postgis 仓库 tag(官网当时不可达);核验不到的以官方仓库为准。

扩展机制本身

先看这台机器上有什么可以装。扩展以文件形式存在于 $PGDATA 同级的安装目录里:

-- 已安装列表
SELECT extname, extversion FROM pg_extension;
-- 可用但未装
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name ILIKE '%vect%' OR name ILIKE '%cron%';

每个扩展对应 $(pg_config --sharedir)/extension/ 下的两类文件:xxx.control(元数据:版本、是否可重入、依赖谁、trusted/restricted 等开关)和 xxx--<版本>.sql(安装脚本,本质是一串 CREATE TYPE / FUNCTION / OPERATOR)。所谓 CREATE EXTENSION,就是在一个事务里执行这些脚本并在目录表登记;控制文件里 requires = 'btree_gist' 这类依赖声明,决定了你要先装谁。PG13 起带 trusted 标记的扩展可由数据库属主(而非超级用户)安装;PG17 起另有 restricted 类标记约束扩展后台 worker 的运行环境——细节以官方 CREATE EXTENSION 文档为准,这里只需知道"非超管装扩展"这条通道存在,权限模型呼应 12-安全与权限管理.md 的最小权限原则。

日常动作四件套:

CREATE EXTENSION IF NOT EXISTS pg_trgm;                  -- 幂等安装
CREATE EXTENSION pgmq SCHEMA mq;                         -- 钉进指定 schema,别污染 public
CREATE EXTENSION vector WITH VERSION '0.8.7';            -- 指定版本(包里有就行)
ALTER EXTENSION vector UPDATE;                           -- 升到 control 里的 default_version

ALTER EXTENSION ... UPDATE 沿扩展自带的版本链逐级执行中间脚本,所以某些扩展不支持跨大版本跳级,只能一路 UPDATE 过去——升级前先看该项目 README 的 upgrade path 说明。

装完之后要知道去哪核对:\dx 列出已装扩展,\dx+ vector 列出该扩展名下的全部成员对象——这一整套正是 CREATE/DROP EXTENSION 批量创建/删除的范围:

\dx
\dx+ vector
-- 当前安装介质里可用的版本与升级路径、以及 superuser/trusted 等标记
SELECT name, version, superuser, trusted, relocatable
FROM pg_available_extension_versions;

注意 DROP EXTENSION 默认 RESTRICT:有对象依赖它(比如列类型是 citext、表用了它的操作符索引)就拒绝删除。加 CASCADE 则是连依赖对象一起删——DROP EXTENSION hstore CASCADE 会把用 hstore 类型的列连同数据一起干掉。生产实例上执行任何带 CASCADE 的 DROP,先按 12-安全与权限管理.md 的规矩用只读账号查一遍 pg_depend 再动手。

哪些扩展需要 shared_preload_libraries

按加载方式分两类,这条分界线直接决定"装它要不要重启":

  • 普通扩展:只往 catalog 里注册类型/函数/操作符,CREATE EXTENSION 即用,随时装卸。pgvector、PostGIS、citext、pg_trgm、hstore、ltree、pgmq、postgres_fdw 都属于这一类。
  • 起后台 worker 的扩展:需要在 postmaster 启动时就把动态库挂进去并注册 worker——pg_cron(调度器进程)、pgAudit(审计)、TimescaleDB(压缩/保留策略的 maintenance daemon)、pg_hint_plan(加载 hint 钩子)这类。没进 preload 列表时,它们的 SQL 接口可能都在,worker 却根本没跑:pg_cron 表现为"任务永远不执行",TimescaleDB 表现为"压缩策略静默不生效",这是最容易被忽略的半残状态。

配置动作(呼应 11-备份恢复与运维.md 的参数管理惯例):

SHOW shared_preload_libraries;                          -- 先看现值!这是一个字符串
ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements,pg_cron';
SELECT pg_reload_conf();                                -- 没用,这类参数要重启
-- 需要完整重启(systemctl restart postgresql 或容器重启)

注意 ALTER SYSTEM SET shared_preload_libraries 是整体覆盖,不是追加。已有 'pg_stat_statements' 却直接 SET 成 'pg_cron',重启后 pg_stat_statements 的统计就没了。先 SHOW 拿现值,逗号拼接后再写回去。

安装途径四选一

  1. 发行版包管理。发行版自带的 postgresql-contrib 提供官方附加包(pg_trgm、citext、hstore、ltree、pgstattuple 等,随 PG 大版本编译)。contrib 之外的第三方(pgvector、TimescaleDB、pg_cron……)走 PGDG 源(00 章配置的 apt.postgresql.org / yum 源),包名形如 postgresql-18-pgvector。PGDG 的好处是二进制与具体大版本对齐、apt upgrade 顺带升级扩展文件;代价是包节奏跟着维护者走。
  2. 源码 + PGXS。没包就自己编:git clone,装到对应 PG 安装树下,make PG_CONFIG=/usr/pgsql-18/bin/pg_config && make install。PGXS 是官方给的"扩展版 Makefile 脚手架",一个 USE_PGXS=1 make 就完成编译安装;源码编译意味着要自己承担 OpenSSL/PROJ 之类依赖和后续每次小版本的重新编译。
  3. 镜像。官方 postgres:18 镜像只带 contrib;常用做法基于它加层装扩展,或选用预装好 pgvector/TimescaleDB 的发行镜像(00 章 Docker 一节的 docker-entrypoint-initdb.d 里直接放 CREATE EXTENSION 脚本即可初始化)。
  4. 云托管白名单。RDS/Aurora、Supabase、各家云 PG 支持的扩展列表互不相同、版本也各自滞后,且基本不允许自己编译。选型前先查所选平台的扩展白名单——这不是流程问题,是硬约束,再好的方案平台不支持就是装不上。

pgvector:RAG 时代的门面

当前最新 0.8.x 系列(tag v0.8.7,实测;HNSW 索引自 0.5.0 引入——0.5.0 的 README 原文标注 “added in 0.5.0”;0.8.x 引入 halfvec/sparsevec 等类型)。它解决的问题:把"语义相似"变成可索引可排序的一等公民。embedding 模型(OpenAI/国产模型皆然)输出一个几百到几千维的 float 数组,相似度检索 = 找距离最近的 K 条——本仓库 agentic 手册的 RAG 章节里,这一步的存储底座就是它。

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_embeddings (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    document_id bigint NOT NULL,                       -- 指回业务文档表
    embedding   vector(1536) NOT NULL,                 -- 维度建表时定死,换模型要换列
    chunk_text  text,
    created_at  timestamptz NOT NULL DEFAULT now()
);

三个距离运算符,选哪个必须和模型训练时的度量一致,混用等于白检索:

运算符 距离 适用
<-> 欧氏距离 L2 图像/部分通用嵌入
<#> 负内积 已归一化的向量上等价于内积
<=> 余弦距离 文本嵌入(OpenAI 类)最常用

索引用 HNSW(多层图)或 IVFFlat(聚类分桶,0.4.x 时代就有的老办法):

-- HNSW:默认 m = 16, ef_construction = 64(0.8.x README 实测口径)
CREATE INDEX ON document_embeddings
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

-- IVFFlat:需要已有数据再建(它靠现有数据训练分桶,lists 经验值见其 README)
CREATE INDEX ON document_embeddings
    USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

选型口径:HNSW 建索引慢、内存高,但同样的召回率下查询快得多,且空表也能建(没有训练步骤);IVFFlat 建得快、省内存,但要先灌入代表性数据再建,且召回对 lists 敏感。新表拿不准就直接 HNSW。两个工程细节:

  • 建索引前把 maintenance_work_mem 调大(会话级 SET maintenance_work_mem = '2GB';),图能整个放进内存时构建速度差别明显;但别高到挤爆服务器——这是 04-索引实战.md “建索引吃内存"与 09-查询优化与EXPLAIN.md “work_mem 溢出"的同一个坑。
  • 查询侧用 SET hnsw.ef_search = 100;(默认 40)换召回率:ef_search 越大越准越慢,top-k 召回不够先动它。

内存吃紧可考虑 halfvec(半精度,元素 2 字节,0.8.x 支持,维度上限 4000;vector(N) 是 4 字节元素,粗算省一半空间),代价是精度损失要在你的评测集上验过再用。

Go 侧用 pgx v5 做相似度检索(08 章的 Query/CollectRows 姿势,vector 用文本字面量直接传,不引第三方类型映射库):

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

type Hit struct {
	DocumentID int64
	Score      float64
}

func main() {
	pool, err := pgxpool.New(context.Background(),
		"postgres://app:secret@localhost:5432/rag?sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	defer pool.Close()

	ctx := context.Background()
	// 真实场景是模型输出的 1536 维,序列化成 '[0.01,-0.02,...]' 文本
	qvec := "[0.01,-0.02,0.03]"

	rows, err := pool.Query(ctx, `
		SELECT document_id, 1 - (embedding <=> $1::vector) AS score
		FROM document_embeddings
		ORDER BY embedding <=> $1::vector
		LIMIT 10`, qvec)
	if err != nil {
		log.Fatal(err)
	}
	hits, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[Hit])
	if err != nil {
		log.Fatal(err)
	}
	for _, h := range hits {
		fmt.Printf("doc=%d score=%.4f\n", h.DocumentID, h.Score)
	}
}

注意 带元数据过滤的向量检索有经典陷阱:WHERE tenant_id = 42 ORDER BY embedding <=> $1 LIMIT 10 这种写法,HNSW 是先取 top-K 再过滤(post-filter)——过滤选择率高时,K 个候选可能几乎全被滤掉,召回断崖。缓解思路要么过滤后再排序(预过滤子查询,代价是退化成全量精排),要么放大 ef_search,新版本也提供了迭代式扫描一类开关,细节以 pgvector 文档实测为准。这一点点到为止,别指望"加了扩展就自动快又准”。

TimescaleDB:把时序分区外包出去

当前 2.30.x 系列(实测 2.30.2,2026-09)。解决什么问题:时序表(metrics、日志、事件流)天然按时间膨胀——写入永远落在最新一段、查询永远带时间范围。07 章教过用原生 PARTITION BY RANGE (ts) 手工挂分区:能跑,但分区要有人按日历创建、老分区要有人清理、压缩要自己做。TimescaleDB 把这三件事变成策略:

-- 前提:已进 shared_preload_libraries 并重启(见上文),再装扩展
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- 普通表直接"提升"为 hypertable:按时间自动切片(chunk),查询语句不变
SELECT create_hypertable('sensor_metrics', 'ts',
       chunk_time_interval => INTERVAL '1 day');

-- 30 天前的数据自动压缩(列式存储,典型省 90% 空间)
ALTER TABLE sensor_metrics SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'device_id'
);
SELECT add_compression_policy('sensor_metrics', INTERVAL '30 days');

-- 一年以上的数据自动丢弃
SELECT add_retention_policy('sensor_metrics', INTERVAL '365 days');

观测入口:hypertable 本体仍是普通表,chunk 存在 _timescaledb_internal 等内部 schema 下;策略任务(压缩/保留/连续聚合)的状态与历史集中在 timescaledb_information.* 视图里,视图名单以官方文档为准——daemon 没跑、策略失败都在这几张视图里现形,这是"静默失效"唯一的解药。

取舍讲明白:hypertable 对应用透明——普通 INSERT/SELECT,规划器自己路由到相关 chunk,这正是它相对 07 章 DIY 分区的最大价值(你不再需要那套"按日历建分区"的 cron 运维)。代价同样明确:必须 preload、要重启;后台 daemon 挂了策略静默失效(监控项 +1);它对新 PG 大版本的支持总是慢半拍,升级主版本前先查 TimescaleDB 的支持矩阵(10-复制与高可用.md/11-备份恢复与运维.md 的升级流程里,这一步不能省);数据文件路径和备份细节也掺进了它自己的目录结构。只是几张表按年分区、量级不大,07 章原生分区就够,不必引入整个后台体系。

PostGIS:地理信息的本行

稳定系列 3.6.x(官方仓库实测最新 tag 3.6.4;3.7.0 处于 RC 阶段)。它解决的问题:经纬度不是数字,是两个坐标系下的位置。04 章讲 GiST 时用过内置 point 的 KNN(ORDER BY location <-> point),那是玩具级;真正的地理需求——“查国贸 5 公里内的门店”——差在两点:内置类型把经纬度当平面坐标算距离,而地球是椭球;行政边界、路网、缓冲区分析根本没有对应类型。

心智模型只有两件事:

  • geometry vs geography:geometry 在投影平面上算,单位随坐标系(4326 下是"度”),快但有畸变;geography 在椭球面上算,单位是米,准但贵。范围小、精度要求一般用 geometry;跨国、跨纬度的距离/面积用 geography。
  • GiST 索引是前置条件:空间谓词(&&、ST_DWithin)靠 GiST 的"包络相交"先粗筛——04 章 lossy 索引、回表重 check 的机制在这里原样适用。
CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TABLE stores (
    id       bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name     text NOT NULL,
    location geography(Point, 4326) NOT NULL     -- 直接存椭球,单位米
);
CREATE INDEX ON stores USING gist (location);

-- 国贸附近 5 公里内的门店,按距离排序
-- 注意:ST_MakePoint 产出的是 geometry,比较前先设 SRID 并显式转成 geography
SELECT name,
       ST_Distance(location, ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography) AS meters
FROM stores
WHERE ST_DWithin(location,
                 ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography, 5000)
ORDER BY location <-> ST_SetSRID(ST_MakePoint(116.4074, 39.9042), 4326)::geography
LIMIT 20;

ST_DWithin 配 geography 的第二参就是米,索引谓词 + KNN 排序各司其职。装完先自证一下运行环境——PostGIS 与 PROJ/GEOS 等外部库版本要配套:

SELECT postgis_full_version();   -- PostGIS/PROJ/GEOS/LWGEOM 版本一屏看全

再往深(坐标转换、栅格、拓扑)就不是速查手册的范围了,装之前先确认你的需求真需要"半个 GIS 服务器"进数据库——PostGIS 依赖 PROJ/GEOS 等外部库,二进制体量不小,也是发行版包最常缺的一环。

全文检索扩展与中文分词现实

07 章的 FTS(生成列 tsvector + GIN)对英文够用:空格天然切词。中文没有空格,PG 内置解析器对汉字基本按单字/整串处理,“北京大学"搜不出"北大”——内置中文分词至今仍是空白,这就是为什么中文站几乎必装一个分词扩展。现实格局:

  • zhparser(amutu/zhparser):老牌方案,基于 SCWS 分词器。近年不活跃——最新 release 是 v2.3(2025-01),其依赖 scws 上游停在 1.2.3 多年没有新版本;词典、纠错靠外部工具。存量系统能用,新系统选型要先接受这个维护节奏。
  • pg_jieba(jaiminpan/pg_jieba):把 jieba 词典搬进 PG,中文圈用得最多的替代者之一;词典加载吃内存,多库实例注意每库一份。
  • pgroonga:Groonga 内核外挂,分词器齐全且自带按键过滤等能力,4.0/4.1 系列发布稳定;体量与运维复杂度也相应高一级。
  • ParadeDB(pg_search):Rust 实现的 BM25 索引,是近年最受关注的方向;项目版本 0.2x 系列(实测最新 tag v0.26.2),较新,特性与稳定性都先在自己的负载上验证再谈上线。

选型建议:国内生产最常见的是 pg_jieba 或 pgroonga,两者都要求你能接手词典运维;云上先查扩展白名单(Supabase 支持哪些、RDS 支持哪些差异很大)。装之前用 07 章的生成列方案把"要不要 FTS、查询长什么样"先想清楚——分词器换的是 token 化,tsvector/GIN 那套骨架不变。

pg_cron:定时任务进了数据库

当前 v1.6.x 系列(实测 v1.6.8,2026-09)。解决什么问题:数据库的内务——清过期分区、归档订单、ANALYZE、刷新物化视图——本来就该贴着数据做。装它要先 preload(见上文——为了几行调度 SQL 先重启一次,值不值也是准入评审的一部分);调度器进程随 postmaster 起,每个任务再由独立 background worker 执行。

CREATE EXTENSION IF NOT EXISTS pg_cron;

-- 每天凌晨 2 点调用 13 章写的归档过程(cron 表达式是 PostgreSQL 方言:分 时 日 月 周)
SELECT cron.schedule('archive-orders', '0 2 * * *',
                     $proc$CALL archive_orders()$proc$);

-- 观测:任务清单与执行历史
SELECT jobid, jobname, schedule, active FROM cron.job;
SELECT jobid, status, return_message, start_time, end_time
FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;

SELECT cron.unschedule('archive-orders');

-- 另一类常见内务:每周对热点表补一次 ANALYZE("先修统计再动查询",见 09 章)
SELECT cron.schedule('analyze-orders', '30 4 * * 0', $$ANALYZE orders$$);

任务默认跑在 cron.database_name 指定的库(默认 postgres);跨库任务要么在目标库里调度(前提是调度账号能连),要么用带 database 参数的 schedule 形式——参数细节以官方 README 为准。

注意 边界要划清:pg_cron 适合数据库内务(VACUUM/ANALYZE 补刀、分区维护、归档、物化视图刷新);业务任务(发优惠券、调第三方接口)别塞进来——它在数据库进程里执行、没有重试/告警/死信语义,失败只留在 cron.job_run_details 里等人看。这类任务属于 asynq/K8s CronJob 的地盘。另外它是单点调度:主备切换后 standby 上不跑 worker,任务跟着新主走即可,别在备库上调度。

队列与 outbox:自建还是 pgmq

pgmq(仓库 github.com/pgmq/pgmq,Tembo 孵化;实测最新 v1.13.1)把 05 章 FOR UPDATE SKIP LOCKED 的队列配方封成开箱接口:

CREATE EXTENSION IF NOT EXISTS pgmq;
SELECT pgmq.create('order_events');                    -- 建队列(内部就是一张表)
SELECT * FROM pgmq.send('order_events',
       '{"order_id": 42, "type": "paid"}'::jsonb);      -- 入队
SELECT * FROM pgmq.read('order_events', 30, 10);        -- 不可见 30s,一批 10 条
SELECT pgmq.archive('order_events', 1);                 -- 处理完归档(DELETE+搬到 archive 表)
SELECT * FROM pgmq.metrics('order_events');             -- 队列深度等观测(签名以官方 README 为准)

它和 DIY 的差别就是"别人替你管好可见性超时、批量、归档"这些模板代码——原理没有新东西,05 章的锁与 13 章的过程语言就是它的全部。DIY 版本对照(队列即表,消息即行):

CREATE TABLE job_queue (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    payload    jsonb NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);
-- 取一条并原子摘走(SKIP LOCKED 让并发 worker 互不踩脚)
DELETE FROM job_queue WHERE id = (
    SELECT id FROM job_queue ORDER BY id
    FOR UPDATE SKIP LOCKED LIMIT 1
) RETURNING id, payload;

选型口径:消息量在百万级、消费者就在应用层,pgmq/自建表完全够,还省一套 MQ 中间件运维;需要通配订阅、复杂路由、跨语言生态,那是 RabbitMQ/Kafka 手册的领域。Outbox 模式一句话——业务写库和"通知外界"必须同一事务落地时,同事务往 outbox 表插一行,外部轮询器(pgmq 消费者、Debezium 皆可)再投递;这是 MQ 手册讲"至少一次"时推荐的数据库侧落点,展开不在此章。

pg_repack:不锁表的"瘦身手术"

当前 1.5.x 系列(实测最新 tag ver_1.5.3)。06 章留过一个钩子:VACUUM FULL 能把膨胀的表重写收缩,但代价是 ACCESS EXCLUSIVE——大表上"几乎永远是错的答案",手动"新表导入 + 改名交换"又累又险。pg_repack 就是这条路的工业化实现:建影子表 → 用触发器搬运原表增量 → 最后短暂拿排他锁改名交换,全程在线,14 章的事件触发器/触发器机制在这里直接复用。

# 收缩单表(也可 --schema 整 schema、-j 并行;--dry-run 先看它会执行什么)
pg_repack -d app -t orders -j 2

# 只重排索引不动堆表
pg_repack -d app -t orders --only-indexes

注意 三个生产门槛:默认需要超级用户(或按文档逐格授予 pg_repack 专用权限,较繁琐);期间空间占用约为原表两份,膨胀 3 倍的表收缩前先确认磁盘;搬运靠触发器,表上已有触发器逻辑要评估,且会写等量 WAL——备库延迟和归档压力要预留(呼应 07 之外的 10-复制与高可用.md 复制延迟观测)。它治的是"已经胖了",防胖还是要回到 06 章的 autovacuum 调参。

规划器的拐杖与观察类

pg_hint_plan:经 shared_preload_libraries 加载(也可会话内 LOAD,但生产要的是实例级一致行为)。09 章的方法论是"估算歪了先修统计、再动查询,enable_* 开关只做诊断";当诊断确认规划器确实走偏、SQL 又来自无法修改的第三方系统时,pg_hint_plan 让你用注释把计划钉死:

/*+ NestedLoop(o u) IndexScan(o orders_created_idx) */
SELECT o.id, u.email FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.created_at > now() - interval '7 days' LIMIT 100;

它是最后的拐杖不是常规武器——hint 会随数据和版本悄悄变脆,每次升级 PG 要回归一遍被钉住的语句。

观察类两个提名:pgBadger(服务器端日志分析工具,把慢查询日志渲染成可视化报告,不在数据库进程内、零入侵,装了扩展也替代不了它);pgAudit(需要 preload;12 章已点名它是合规审计的社区标准答案,pgaudit.log = 'ddl, write' 按语句类别记录,对象级细粒度要靠 pgaudit.role 授权策略——具体语义以官方 README 为准)。两者一个看性能、一个看行为,都是出事后救火队。

小而美的 contrib 一览

这些是随 PG 编译好的官方附加包(contrib 或独立包),单看每个都不大,合起来构成"少写八百行代码"的底气:

扩展 解决什么 要点与呼应
citext 大小写不敏感的文本列 02 章 users.email text UNIQUE 登录大小写混用的正解之一:CREATE EXTENSION citext; 后 ALTER TABLE users ALTER COLUMN email TYPE citext;——改类型会重建该列上的唯一约束/索引并拿 ACCESS EXCLUSIVE 锁,大表按 02 章改表代价表评估。不想动类型,就保留 text + CREATE UNIQUE INDEX ON users (lower(email));(04 章表达式索引),插入侧自己归一化
hstore KV 文本(jsonb 出现前的方案) 新表一律 jsonb(02/07 章),hstore 只在存量迁移时认识即可;它体积小、无 JSON 解析开销,个别纯 KV 场景仍有拥趸
ltree 层级路径 a.b.c 类型 07 章递归 CTE 的 referrer 树若查询恒定是"某节点及其所有后代",ltree + GiST/GIN 能把递归变成一次前缀匹配;代价是树结构冗余存进每行,移动节点要整片改
tablefunc crosstab 行转列 报表宽表输出偶尔救命,能用聚合+CASE 写就别引它
uuid-ossp uuid_generate_v4() 等 历史包袱:优先内置 gen_random_uuid()(PG13+)与 PG18 uuidv7()(02 章主键选型、16 章随机 UUID 之痛);新系统装 uuid-ossp 的唯一理由是老代码迁移
pgstattuple / pg_freespacemap / pg_visibility 空间与可见性观测 06 章膨胀实验的仪表盘:pgstattuple 报死元组占比、pg_freespacemap 看空闲图、pg_visibility 对照 VM 与页面真实可见性——排"vacuum 好像没生效"时按需选用,均需显式安装
pg_trgm 三元组模糊匹配 similarity(name,'...') + GiST/GIN 支撑"像不像"查询;04 章 GiST 的又一战场,中文模糊搜索慎用(按字节切三元组,对 UTF-8 汉字效果一般)
btree_gist / btree_gin 把 B-tree 语义搬进 GiST/GIN 04 章 EXCLUDE USING gist 防超卖例子里已经 CREATE EXTENSION btree_gist——复合约束里 int/text 进 GiST 全靠它
pg_stat_statements SQL 级统计 严格说也是 contrib:09 章找"最耗时的语句"从它开始,但它才是真正需要 preload 的常驻项,多数生产库应默认开启

FDW:跨库查询,别当分库中间件

外部数据包装器(Foreign Data Wrapper)让 PG 把"别处的东西"当表查。用得最多的是 postgres_fdw(跨 PG 实例),最小四步:

CREATE EXTENSION postgres_fdw;

CREATE SERVER shop_remote FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.0.21', port '5432', dbname 'shop');

CREATE USER MAPPING FOR app SERVER shop_remote
    OPTIONS (user 'readonly', password '****');

IMPORT FOREIGN SCHEMA public LIMIT TO (orders, products)
    FROM SERVER shop_remote INTO fdw;

SELECT count(*) FROM fdw.orders WHERE status = 'paid';

之后它就是一张"看起来能查的表",适合低频跨库取数、报表聚合、灰度迁移期双查。但要认清三条边界:

  • 下推有范围:简单的 WHERE/投影会翻译成远端 SQL 下推执行,JOIN/聚合/排序是否下推、下推得聪明与否,都要用 EXPLAIN (VERBOSE) 看远端实际收到了什么——09 章读 plan 的功夫在这里直接变现。跨实例网络一抖,延迟就是两个实例之和。
EXPLAIN (VERBOSE, COSTS OFF)
SELECT count(*) FROM fdw.orders WHERE status = 'paid';
-- 重点看 Foreign Scan 节点里的远端 SQL:FILT(过滤条件)出现在远端语句里才算下推成功;
-- 若远端是 SELECT *,整表拉回本地再过滤,高频查询考虑本地物化视图或干脆导数
  • 没有真正的分布式事务:远端写不进本地事务的原子性(PG 没有内置 2PC 管理器跨 FDW 兜底),别拿它当分库分表中间件,也别指望"两张 FDW 表 JOIN 出一个一致性结果"。
  • USER MAPPING 存的是口令(可被有权限者读取),给远端账号最小权限——12 章的规矩。

file_fdw:把服务器上的 CSV/日志映射成只读表,批量导入前的探查很好用。dblink:更古老的跨库调用方式,连接是"每次函数调用自己开一条"的散兵游勇,事务语义、超时、凭据管理都丑——存量认得即可,新代码一律 postgres_fdw。

过程语言扩展:u 代表 untrusted

PL/pgSQL(13 章)之外,PG 支持把 Python/Perl/TCL/Lua 等挂成存储过程语言。名字结尾带 u 的——plpython3u、plperlu、pltcl——是 untrusted:代码能读写服务器文件系统、开网络连接、调任意库。创建 untrusted 语言需要超级用户权限(12 章),这意味着:装了它,数据库的权限边界就靠"谁能建函数、函数里写了什么"人肉把关;SQL 注入点从查询转移到宿主语言字符串拼接,扫描器更看不住。公司环境原则上不装;真要 Python 生态(数据科学胶水),优先评估"应用层调、数据库只存数据"的替代路径。plv8(JS 引擎)只提名一句:曾红极一时、维护节奏起伏大,新项目选型先查仓库现状;顺带一提 PG18 把 JSON 处理等能力持续内置化,很多当年用 untrusted 语言打的补丁已有正解。

选型与运维总表

需求 首选 要 preload? 主要运维成本 呼应章节
向量相似检索(RAG) pgvector 0.8.x 否 索引内存/构建调参、过滤召回 04、08、09
时序分区+压缩+保留 TimescaleDB 2.30.x 是 重启、daemon 监控、大版本跟进 07、11
地理空间 PostGIS 3.6.x 否 外部依赖库、坐标系心智 04
中文全文检索 pg_jieba / pgroonga 否 词典运维、内存 07
数据库内务定时任务 pg_cron v1.6.x 是 重启、观测 job_run_details 13
轻量队列/outbox pgmq v1.13.x 或自建表 否 SKIP LOCKED 语义要懂 05、13
在线收缩膨胀表 pg_repack 1.5.x 否 双份空间、触发器开销 06、14
钉死走偏的执行计划 pg_hint_plan 是 hint 脆弱、升级需回归 09
合规审计 pgAudit 是 日志量激增 12
跨 PG 取数 postgres_fdw 否 无分布式事务、凭据管理 09、12
大小写不敏感 email citext 否 改类型锁与重建 02、04

扩展与升级、备份的相互作用

三件事必须提前知道:

  • pg_upgrade 不升级扩展内容。大版本升级把旧集群的 catalog 原样搬来:新集群的二进制/脚本装了新版本(比如 OS 包已经到 pgvector 0.8.7),库里登记的还是旧版本号,且 pg_upgrade 会校验每个已装扩展在新集群是否存在同版本文件——老版本文件不在包里时直接拒绝升级。所以流程是:升级前把旧库扩展 ALTER EXTENSION ... UPDATE 到新包也覆盖的版本,或走 dump/restore;升级后逐库核对再统一 UPDATE。一条"落后清单"SQL 替你巡检(当前版本 vs 介质里的默认版本):
SELECT e.extname, e.extversion AS current, a.default_version AS available
FROM pg_extension e
JOIN pg_available_extensions a ON a.name = e.extname
WHERE e.extversion <> a.default_version;

细节与回滚预案见 11-备份恢复与运维.md 的大版本升级路线。

  • pg_dump 把扩展当一个对象。属于扩展的成员对象(它的类型、操作符、函数)不单独导出,恢复时整体执行 CREATE EXTENSION;--exclude-extension 可排除(官方文档自 PG17 起提供,实测)——跨实例迁移时想不带某个扩展就靠它。注意 postgres_fdw 的 SERVER/USER MAPPING 不是扩展创建的成员对象,灾备/迁移时要么随 dump 走、要么手工先建好,恢复顺序按"SERVER → 扩展与外部表 → 数据"理顺。
  • 恢复侧要有二进制。dump 里一句 CREATE EXTENSION vector 只认库文件;目标实例没装对应版本的包,恢复直接报错。备份文件与目标环境的扩展清单要配套维护——这也是把扩展列表纳入配置管理(哪怕一张 Markdown 表格)的理由。

实战建议

生产实例上任何 CREATE EXTENSION(和 DROP)都应当走变更评审,和发版同级——它改的是数据库进程本身的行为面。准入先问三个问题:

  1. 谁维护? 看最近 release 日期、issue 响应、有没有商业主体或大公司在用。pgvector(0.8.x 持续迭代)、pg_cron(v1.6.x)、TimescaleDB(2.30.x)、pg_repack(1.5.x)、PostGIS(3.6.x)节奏都健康;zhparser 及其 scws 依赖链低活跃要自己掂量;ParadeDB 这类 0.2x 阶段的项目,先在旁路环境把负载跑穿一轮再谈上线。
  2. 要不要 preload? 要 preload 意味着装它要重启、每次 PG 小版本维护窗口都要确认它跟上了新版本编译。业务核心库优先选 CREATE EXTENSION 即用的扩展;非 preload 不可的功能(定时任务、审计)能外置(asynq、应用层拦截器)则外置。
  3. 出事故能否绕过它先降级? 装之前想好"这个扩展关掉/降级后系统怎么活":pg_hint_plan 可以摘 hint,TimescaleDB 可以停策略,pg_cron 可以 unschedule 挪回外部调度;而 PostGIS、citext 这类进了数据模型的(列类型、空间索引)几乎不可逆——越靠近数据的扩展,越要一次做对。

最后两条纪律。其一,别收集瑞士军刀:每装一个扩展就多一分攻击面(12 章的审计清单里,函数与扩展权限常年被漏查)、多一笔升级债(每个都要在 pg_upgrade 前逐版本核对,见上节)。够用即止。其二,把扩展当成代码对待:版本写进配置管理、升级走变更流程、上线后在 pg_extension 与日志两侧都留观测——本章所有"装它要付出什么"最终都会变成你告警群里的消息,提前想好谁来读。