# PostgreSQL 扩展机制

LLMS 索引： [llms.txt](/llms.txt)

---

`CREATE EXTENSION vector;` 只有一行，却横跨文件系统、权限系统、版本图和
数据库依赖。要治理扩展，必须先把这一行展开。

本节讨论 PostgreSQL 自己知道什么、不会替你知道什么。软件包仓库、容器与
Pigsty 映射留到后面。

## 14.1.1 control、SQL 脚本、动态库与对象所有权 {#item-14-1-1}

### 一套扩展至少有两个身份

假设执行：

```sql
CREATE EXTENSION vector
WITH SCHEMA app_ext
VERSION '0.8.4';
```

这里的 `vector` 是 **SQL 扩展名**。它不是项目名 `pgvector`，也不必等于
RPM/DEB 包名。PostgreSQL 根据 server 自己的安装目录寻找：

```text
$(pg_config --sharedir)/extension/vector.control
$(pg_config --sharedir)/extension/vector--0.8.4.sql
$(pg_config --pkglibdir)/vector.so       # Linux 常见
$(pg_config --pkglibdir)/vector.dylib    # macOS 可能出现
```

典型扩展由三类文件组成：

| 文件 | 作用 | PostgreSQL 何时使用 |
|---|---|---|
| `name.control` | 元数据、默认版本、权限、依赖、可迁移性 | 发现与创建/更新扩展 |
| `name--version.sql` | 创建这一数据库版本的成员对象 | `CREATE EXTENSION` |
| `name--old--new.sql` | 从一个对象版本迁到另一个版本 | `ALTER EXTENSION UPDATE` |
| 动态库 | C 函数、hook、访问方法等运行代码 | 创建时或 backend 加载/调用时 |

纯 SQL 扩展可以没有动态库；只提供可加载模块的组件也可能没有
`CREATE EXTENSION` 接口。不能从扩展名猜文件组合，要读 control 与目标版本
说明。

查看当前 server 的查找位置：

```bash
pg_config --sharedir
pg_config --pkglibdir
pg_config --version
```

这里的 `pg_config` 必须属于目标 server major。用 PATH 中另一个 PostgreSQL
版本的 `pg_config` 检查文件，可能得到一个完全正确、却与正在运行实例无关
的目录。

### control 文件是创建合同

一个简化 control 文件可能表达：

```ini
comment = 'example data type'
default_version = '1.4'
module_pathname = '$libdir/example'
relocatable = true
superuser = true
trusted = false
requires = 'btree_gist'
```

关键字段：

- `default_version`：未写 `VERSION` 时创建哪个对象版本；
- `module_pathname`：版本 SQL 中 `MODULE_PATHNAME` 的替换值，常指向
  `$libdir` 下动态库；
- `requires`：必须先存在的其他扩展；
- `superuser`：安装脚本是否原则上要求超级用户；
- `trusted`：在 `superuser=true` 的前提下，是否允许具备当前数据库
  `CREATE` 权限的非超级用户安装；
- `relocatable`：扩展整体是否允许换 schema；
- `schema`：若指定，强制成员安装到该 schema，并使扩展不可随意迁移。

这些是 **具体已安装 control 文件** 的事实，不是扩展项目永久不变的属性。
同名扩展升级后可以改变元数据，发行版也可能带不同补丁。用目录视图读取
当前 server 实际看到的值：

```sql
SELECT
    name,
    version,
    installed,
    superuser,
    trusted,
    relocatable,
    schema,
    requires
FROM pg_available_extension_versions
WHERE name IN ('pg_trgm', 'vector')
ORDER BY name, version;
```

本章正式夹具观测到：

```text
pg_trgm 1.3..1.6  superuser=t trusted=t relocatable=t
vector  0.8.4     superuser=t trusted=f relocatable=t
```

这是 PostgreSQL 18.6 + 当前本地支持文件的证据；读者环境必须重查。

### `CREATE EXTENSION` 创建的是一个依赖边界

版本 SQL 可以创建类型、函数、操作符、访问方法、opclass、表或其他对象。
PostgreSQL 把这些对象登记为扩展成员。扩展本身记录在：

```sql
SELECT
    e.extname,
    e.extversion,
    pg_get_userbyid(e.extowner) AS owner,
    n.nspname AS nominal_schema,
    e.extrelocatable
FROM pg_extension AS e
JOIN pg_namespace AS n
  ON n.oid = e.extnamespace;
```

成员关系记录在 `pg_depend`，依赖类型为 `e`：

```sql
SELECT
    d.classid::regclass AS member_catalog,
    count(*) AS members
FROM pg_extension AS e
JOIN pg_depend AS d
  ON d.refclassid = 'pg_extension'::regclass
 AND d.refobjid = e.oid
 AND d.deptype = 'e'
WHERE e.extname = 'pg_trgm'
GROUP BY d.classid
ORDER BY member_catalog::text;
```

这个关系有三个后果：

1. 成员通常不能绕过扩展被单独删除；
2. `DROP EXTENSION` 会删除成员，即使没有写 `CASCADE`；
3. `pg_dump` 通常用 `CREATE EXTENSION` 重建整组对象，而不是逐个 dump
   成员定义。

第三点非常重要：备份文件能记录“需要 vector 0.8.4 的对象合同”，却不会
把 control、SQL 脚本和动态库塞进备份。恢复目标必须先拥有兼容支持文件。

### nominal schema 不是容器

`pg_extension.extnamespace` 常被叫作扩展 schema，但它不是一个不可穿透的
容器。官方文档明确指出，扩展成员可能位于多个 schema；这个字段表示扩展
的 nominal schema。扩展名本身也不受 schema 限定：

```sql
-- 一个数据库中只能有一个同名 extension
CREATE EXTENSION pg_trgm SCHEMA app_ext;

-- 这不是合法的第二份“另一个 schema 的 pg_trgm”
CREATE EXTENSION other.pg_trgm;
```

`relocatable=true` 也不表示所有业务依赖都能无痛迁移。换 schema 会影响：

- 未全限定的函数与操作符解析；
- 固化在 view、expression index 或 function body 中的对象引用；
- 应用 `search_path`；
- dump/restore 与旧迁移脚本；
- 安全审计假设。

把“目录允许 `ALTER EXTENSION ... SET SCHEMA`”与“应用兼容迁移”分开验证。

### 扩展 owner 与成员 owner

扩展有自己的 owner。通常创建者成为 extension owner；但安装脚本内部成员
的所有权还受脚本、可信安装规则和 PostgreSQL 版本语义影响，不能简单假设
“schema owner 拥有其中一切”。

本章把 `pg_trgm` 装在 `shop_ch14`：

```sql
SET ROLE pg36_owner;
CREATE EXTENSION pg_trgm
  WITH SCHEMA shop_ch14
  VERSION '1.3';
RESET ROLE;
```

由于本地 control 标记 `trusted=true`，有数据库 `CREATE` 权限的
`pg36_owner` 可以安装，extension owner 是 `pg36_owner`。安装脚本在受控
的高权限上下文中执行，使扩展能够创建所需对象。

这不是把任意第三方 SQL 交给普通用户安全执行。`trusted` 是扩展供应者和
发行者作出的安全承诺，管理员仍要审查来源、版本与安装 schema。

参见 [Packaging Related Objects into an Extension](https://www.postgresql.org/docs/18/extend-extensions.html)
和 [`pg_extension`](https://www.postgresql.org/docs/18/catalog-pg-extension.html)。

## 14.1.2 普通扩展、预加载库与超级用户需求 {#item-14-1-2}

### “已创建”不等于“已加载”

按运行方式可以粗分四类：

| 类型 | 支持文件 | `CREATE EXTENSION` | preload/restart |
|---|---|---|---|
| 纯 SQL 扩展 | control + SQL | 通常需要 | 通常不需要 |
| 按需加载的 C 扩展 | control + SQL + library | 通常需要 | 首次调用可加载 |
| 需要早期 hook 的扩展 | control + SQL + library | 通常需要 | 常需 shared preload |
| 非 SQL 插件 | library 或可执行组件 | 可能不需要 | 按子系统规则使用 |

扩展若要在 backend 初始化早期注册 shared memory、planner/executor hook、
background worker 或全局审计能力，往往需要：

```conf
shared_preload_libraries = '...'
```

这个参数在 server 启动时处理。修改后 reload 不够，通常需要滚动重启或
集群重启。库名拼错、文件缺失或二进制不兼容，可能直接阻止实例启动，所以
它是比普通 `CREATE EXTENSION` 更高风险的变更。

检查声明与 live 值：

```sql
SELECT
    name,
    setting,
    source,
    sourcefile,
    pending_restart
FROM pg_settings
WHERE name = 'shared_preload_libraries';
```

只看配置仓库中的 YAML 不够；只看 `SHOW` 也不够。前者是意图，后者是当前
实例事实，还要在所有主备节点检查动态库。

`pg_trgm` 与本章的 `vector` 0.8.4 不需要 shared preload。不能由此推断
其他版本或扩展也不需要。Pigsty 当前文档列举 Citus、TimescaleDB、
`pg_cron`、`pgaudit` 等常见预加载场景，最终以目标扩展/版本文档和启动
实验为准。

### `superuser` 与 `trusted` 是两道判断

`pg_available_extension_versions` 中：

```text
superuser=true, trusted=false
```

表示只有超级用户可以执行创建/更新脚本。`vector` 在本章环境属于这一类：

```sql
SET ROLE pg36_owner;
CREATE EXTENSION vector
  WITH SCHEMA shop_ch14
  VERSION '0.8.4';
```

得到：

```text
SQLSTATE 42501
permission denied to create extension "vector"
HINT: Must be superuser to create this extension.
```

管理员安装后：

```sql
CREATE EXTENSION vector
  WITH SCHEMA shop_ch14
  VERSION '0.8.4';
```

extension owner 保持管理员角色。应用只获得使用所需类型、函数和表权限，
不获得扩展所有权。

对：

```text
superuser=true, trusted=true
```

有数据库 `CREATE` 权限的非超级用户可以安装。安全关键点是：

- 安装脚本以 bootstrap superuser 的能力执行；
- extension owner 是调用者；
- 供应者必须保证非特权调用者不能借脚本选择、schema 或预置对象提权；
- 管理员应只信任随受控 PostgreSQL 发行版交付、且明确标记 trusted 的版本。

这就是为什么官方 `CREATE EXTENSION` 文档警告：从不可信来源安装扩展，相当
于以高权限运行其安装脚本。版本 SQL 能执行 DDL，也可能引用安装 schema
中预先存在的对象；安全安装应使用受控 schema 和受控 `search_path`。

### 权限分层

推荐把角色拆开：

```text
platform/admin
  ├─ installs OS packages on every node
  ├─ changes preload and restarts
  ├─ creates untrusted extensions
  └─ owns privileged extension lifecycle

NOLOGIN database owner
  ├─ owns application schemas/tables
  ├─ may own reviewed trusted extensions
  └─ runs migrations through controlled SET ROLE

application login
  ├─ uses selected functions/operators/types
  └─ cannot CREATE/ALTER/DROP EXTENSION
```

本章证明：

```sql
-- pg36_app
ALTER EXTENSION pg_trgm UPDATE TO '1.6';
```

返回：

```text
SQLSTATE 42501
must be owner of extension pg_trgm
```

应用能使用扩展能力，并不需要拥有生命周期控制权。

### 创建失败要按层定位

常见错误及第一检查点：

| 错误 | 更可能是哪一层 | 首查 |
|---|---|---|
| extension is not available | control 文件不可见 | `pg_available_extensions`、server `sharedir` |
| could not access file `$libdir/...` | 动态库缺失/路径错 | `pg_config --pkglibdir`、各节点文件 |
| must be superuser | control 权限合同 | `pg_available_extension_versions` |
| must be loaded via shared_preload_libraries | 进程初始化条件 | `pg_settings`、重启状态、日志 |
| extension already exists | 数据库作用域冲突 | `pg_extension` |
| no installation script for version | 支持文件/版本图不完整 | available versions、包版本 |
| incompatible library | server major/CPU/ABI 错配 | 包构建、PG major、架构、启动日志 |

不要用反复 `CREATE EXTENSION ... CASCADE` 猜答案。先把错误归到供应、进程或
数据库层。

## 14.1.3 扩展依赖、版本与 `ALTER EXTENSION` {#item-14-1-3}

### `requires` 是扩展依赖，不是 OS 包依赖

control 中：

```ini
requires = 'foo, bar'
```

表示数据库内扩展依赖。创建前可以显式安装：

```sql
CREATE EXTENSION foo;
CREATE EXTENSION bar;
CREATE EXTENSION target;
```

也可以：

```sql
CREATE EXTENSION target CASCADE;
```

但生产治理不应默认 `CASCADE`，因为它会：

- 选择依赖的默认版本；
- 使用当前 schema/search path 决定安装位置；
- 扩大实际变更集合；
- 让审批只写一个扩展，实际多装若干对象。

更可审计的做法是显式列出依赖顺序、版本、schema、owner 和每步验证。

数据库依赖也不替代 OS 包依赖。目标 control 文件声明需要 `foo`，但节点上
仍必须先安装提供 `foo.control`、SQL 和 library 的软件包。

### 软件支持版本与对象版本分离

假设节点刚装入支持 1.6 的新包，而数据库仍显示：

```sql
SELECT extversion
FROM pg_extension
WHERE extname = 'pg_trgm';

-- 1.3
```

此时：

```text
filesystem supports: 1.3, 1.4, 1.5, 1.6
database objects are: 1.3
```

这是正常的中间状态，不是 PostgreSQL 自动遗漏升级。装包不会主动在每个
数据库执行对象迁移；管理员必须逐库评审：

```sql
ALTER EXTENSION pg_trgm UPDATE TO '1.6';
```

先列出版本：

```sql
SELECT
    name,
    version,
    installed,
    superuser,
    trusted,
    relocatable
FROM pg_available_extension_versions
WHERE name = 'pg_trgm'
ORDER BY string_to_array(version, '.')::integer[];
```

再检查更新图：

```sql
SELECT source, target, path
FROM pg_extension_update_paths('pg_trgm')
WHERE source = '1.3'
   OR target = '1.6'
ORDER BY source, target;
```

本章得到：

```text
1.3 -> 1.6 : 1.3--1.4--1.5--1.6
```

PostgreSQL 会按可用更新脚本寻找路径；路径不是任意版本号比较。若没有从
当前对象版本到目标版本的脚本链，更新就不能发生。所谓“降级”同样需要明确
反向脚本，不能假设 `ALTER EXTENSION ... TO old` 会还原。

### 更新是 DDL 事务，不是无风险元数据改名

更新脚本可执行 DDL/DML，可能：

- 替换函数、操作符与类型支持函数；
- 增删成员；
- 改写扩展配置表；
- 获取对象锁；
- 使依赖表达式、索引或 cached plan 失效；
- 对大表触发长时间工作。

扩展脚本在一个隐式事务中运行，不能在其中自行提交，也不能把需事务外执行
的操作当普通更新步骤。即使脚本通常很快，也要把它当 schema migration：

```text
freeze target package build
  -> read release notes and update scripts
  -> clone/restore rehearsal
  -> dependency and lock inspection
  -> behavior baseline
  -> ALTER EXTENSION UPDATE
  -> catalog + query + plan + log verification
  -> observation window
```

更新 owner 才能 `ALTER EXTENSION`；某些脚本还因 control 权限需要更高
特权。本章由 `pg36_owner` 更新自己拥有的 trusted `pg_trgm`，而不是给
应用角色临时提权。

### 成员变化与 dump 语义

扩展维护者可以用：

```sql
ALTER EXTENSION name ADD object;
ALTER EXTENSION name DROP object;
```

调整成员关系。这不是业务迁移的日常捷径。成员一旦归入扩展：

- dump 通常不再单独保存其定义；
- `DROP EXTENSION` 会删除它；
- 随手修改成员定义可能不会按预期进入 dump；
- 正确升级应通过新的扩展版本和 update script 交付。

检查成员，而不是只看 `\dx`：

```sql
SELECT
    e.extname,
    d.classid::regclass AS catalog,
    count(*) AS members
FROM pg_extension AS e
JOIN pg_depend AS d
  ON d.refclassid = 'pg_extension'::regclass
 AND d.refobjid = e.oid
 AND d.deptype = 'e'
GROUP BY e.extname, d.classid
ORDER BY e.extname, catalog::text;
```

本章 `pg_trgm` 1.3 有 37 个登记成员，更新到 1.6 后为 47；`vector`
0.8.4 在 PostgreSQL 18.6 夹具中有 237 个。成员数量用于发现本次环境漂移，
不是跨 PG major 的普适 golden。

### 本节结论

把 `CREATE EXTENSION` 读成一条完整声明：

```text
using support files from this exact server installation,
run this reviewed version script with this privilege model,
create one database-scoped extension owned by this role,
attach these member objects and dependencies,
and promise that future dump/restore/update can obtain matching files.
```

少掉任何一段，扩展都只是“今天在这台主机上能用”，还不是可运营能力。

---

[返回本章目录](../) · [下一节：内核、发行版与托管服务](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
