扩展系统与 FDW:PG 的 hook 机制如何让扩展影响 Planner 决策
如果你用过 postgres_fdw
跨库查询,大概率遇到过这样一个问题:一个简单的
SELECT * FROM remote_t WHERE id = 1
跑得很慢,EXPLAIN (VERBOSE) 一看,发现 WHERE
条件根本没推到远端——PG
把远程表数据全拉回本地,然后在本地做过滤。
这不是 bug,这是扩展需要显式实现 pushdown 逻辑的结果。PG 的扩展系统给外部代码插入了 planner 和 executor 的决策路径,但能插到什么深度、改变了哪些决策,取决于你用的 hook 在哪个层次。
本文从两套并行机制拆解 PG 扩展体系:全局 hook
机制(让扩展在七个子系统中插入自定义逻辑)和 FDW 的
FdwRoutine 回调接口(让外部表参与 planner
的代价估算与路径生成)。理解这两套机制,你就能判断一个扩展”到底改了
PG 的什么行为”,以及 postgres_fdw 的 pushdown 为什么有些 SQL
能推、有些不能推。
一、扩展的 SQL 入口:CREATE EXTENSION 与 .control 文件
1.1 CREATE EXTENSION 的执行流程
在 SQL 层面,扩展安装只需要一条命令:
CREATE EXTENSION postgres_fdw;
```text
这条命令背后的执行路径在 `src/backend/commands/extension.c` 的 `CreateExtension()` 中:
```text
CreateExtension()
→ 读取 .control 文件 → 解析扩展元数据
→ 检查依赖(requires 字段)和版本兼容
→ parse_extension_script() → 解析 SQL 安装脚本
→ 依次执行每条 SQL(CREATE FUNCTION、CREATE FOREIGN DATA WRAPPER 等)
→ InsertExtensionTuple() → 写入 pg_extension 系统表
→ 注册扩展 OID,供 DROP EXTENSION 级联清理PG 在启动时不会扫描所有
.control 文件。扩展的注册是惰性的——只在
CREATE EXTENSION
时按需读取。没有安装的扩展不占用任何内存。
// src/backend/commands/extension.c, CreateExtension() 的简化核心逻辑
ObjectAddress CreateExtension(ParseState *pstate,
const char *extensionName,
bool if_not_exists, ...)
{
// 1. 读取 .control 文件
control = read_extension_control_file(extensionName);
// 2. 检查依赖扩展和版本
check_extension_dependencies(control);
// 3. 解析并执行 SQL 安装脚本
sql = read_extension_script_file(extensionName, control);
execute_extension_script(sql);
// 4. 写入 pg_extension
InsertExtensionTuple(extensionName, ...);
return address;
}
```bash
### 1.2 .control 文件格式
`.control` 文件放在 `SHAREDIR/extension/` 下(如 `/usr/share/postgresql/17/extension/`),文件名与扩展名一致。以 `postgres_fdw.control` 为例:
```conf
# postgres_fdw.control
comment = 'foreign-data wrapper for remote PostgreSQL servers'
default_version = '1.1'
module_pathname = '$libdir/postgres_fdw'
relocatable = true关键字段:
| 字段 | 含义 |
|---|---|
comment |
扩展描述,显示在 \dx 输出中 |
default_version |
默认安装版本,对应 extension--version.sql
脚本 |
module_pathname |
共享库(.so)路径,$libdir 指向 PG 的
pkglibdir |
relocatable |
是否可通过 ALTER EXTENSION ... SET SCHEMA
迁移 schema |
requires |
依赖扩展列表 |
trusted |
PG 13+:非超级用户是否可安装(true /
false) |
1.3 对应 SQL 安装脚本
.control 中的
default_version = '1.1' 指示 PG 加载
postgres_fdw--1.1.sql:
-- postgres_fdw--1.1.sql(简化摘录)
CREATE FUNCTION postgres_fdw_handler()
RETURNS fdw_handler
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;
CREATE FUNCTION postgres_fdw_validator(text[], oid)
RETURNS void
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT;
CREATE FOREIGN DATA WRAPPER postgres_fdw
HANDLER postgres_fdw_handler
VALIDATOR postgres_fdw_validator;
```text
**两步注册模式**是所有 FDW 扩展的标准做法:先注册 C 函数(`fdw_handler` 和可选的 `validator`),再用这些函数创建 `FOREIGN DATA WRAPPER` 对象。`handler` 函数是 FDW 的核心——当 PG 需要访问外部表时,调用 handler 获取 `FdwRoutine` 结构体,其中包含所有回调函数指针。
### 1.4 _PG_init() 与 shared_preload_libraries
C 扩展的共享库被加载时,PG 自动调用其 `_PG_init()` 函数。这是扩展注册全局 hook 的唯一窗口——所有 hook 注册必须在此完成。
`_PG_init()` 的执行时机取决于扩展的加载方式:
- **`shared_preload_libraries`**:Postmaster 启动时,fork 任何子进程之前执行。此时 `MyProc` 不可用、不能执行事务。`shmem_startup_hook` 只能在这时注册。
- **`session_preload_libraries` / `local_preload_libraries`**:每个 Backend 进程的 `InitPostgres()` 阶段执行,此时事务可用。`planner_hook`、`ExecutorStart_hook` 等可在此注册。
- **`CREATE EXTENSION`**:仅当前 session 加载 `.so` 并调用 `_PG_init()`,其他已有 session 不受影响。
---
## 二、Hook 机制全景:PG 七个关键注入点
PG 的内核代码在约 30 个关键位置预留了全局函数指针——初始值为 `NULL`,扩展在 `_PG_init()` 中将其替换为自己的包裹函数,在自定义逻辑中可调用原始函数完成链式执行。
### 2.1 Planner 层 Hook
```c
// src/include/optimizer/planner.h
typedef PlannedStmt *(*planner_hook_type)
(Query *parse, const char *query_string,
int cursorOptions, ParamListInfo boundParams);
extern PGDLLIMPORT planner_hook_type planner_hook;
// src/include/parser/analyze.h
typedef void (*post_parse_analyze_hook_type)
(ParseState *pstate, Query *query, JumbleState *jstate);
extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;planner_hook 是最彻底的干预点——它替换整个
planner() 调用。pg_stat_statements
用它捕获 query jumble 和 queryid,Citus 和 TimescaleDB
用它实现分布式查询规划。
post_parse_analyze_hook
在语义分析完成后触发,允许修改 parse
tree。pg_hint_plan 用它插入 hint。
2.2 Executor 层 Hook
// src/include/executor/executor.h
typedef void (*ExecutorStart_hook_type)(QueryDesc *queryDesc, int eflags);
extern PGDLLIMPORT ExecutorStart_hook_type ExecutorStart_hook;
typedef void (*ExecutorRun_hook_type)
(QueryDesc *queryDesc, ScanDirection direction,
uint64 count, bool execute_once);
extern PGDLLIMPORT ExecutorRun_hook_type ExecutorRun_hook;
typedef void (*ExecutorFinish_hook_type)(QueryDesc *queryDesc);
extern PGDLLIMPORT ExecutorFinish_hook_type ExecutorFinish_hook;
typedef void (*ExecutorEnd_hook_type)(QueryDesc *queryDesc);
extern PGDLLIMPORT ExecutorEnd_hook_type ExecutorEnd_hook;
```text
四个 hook 分别包裹执行器的四个阶段。`auto_explain` 注册 `ExecutorStart_hook` 和 `ExecutorEnd_hook`,在查询执行前后记录 `log_min_duration` 和 EXPLAIN 输出。
### 2.3 ProcessUtility Hook
```c
// src/include/tcop/utility.h
typedef void (*ProcessUtility_hook_type)
(PlannedStmt *pstmt, const char *queryString,
bool readOnlyTree, ProcessUtilityContext context,
ParamListInfo params, QueryEnvironment *queryEnv,
DestReceiver *dest, QueryCompletion *qc);
extern PGDLLIMPORT ProcessUtility_hook_type ProcessUtility_hook;ProcessUtility_hook 覆盖所有 DDL 和 utility
命令——CREATE TABLE、VACUUM、ALTER SYSTEM、CHECKPOINT
等。pg_stat_statements 用它追踪 DDL 的
queryid,sepgsql 用它做 DDL 权限检查。注意此
hook 独占整个 utility
分发路径——多个扩展不能同时注册,只能通过链式调用来兼容。
2.4 路径级 Hook(Pathlist 干预)
// src/include/optimizer/paths.h
typedef void (*set_rel_pathlist_hook_type)
(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte);
extern PGDLLIMPORT set_rel_pathlist_hook_type set_rel_pathlist_hook;
typedef void (*set_join_pathlist_hook_type)
(PlannerInfo *root, RelOptInfo *joinrel,
RelOptInfo *outerrel, RelOptInfo *innerrel,
JoinType jointype, JoinPathExtraData *extra);
extern PGDLLIMPORT set_join_pathlist_hook_type set_join_pathlist_hook;
```text
这两个 hook 是最细粒度的 planner 干预点:`set_rel_pathlist_hook` 在 planner 为某个基表创建完路径后被调用,允许扩展添加额外路径或修改路径代价。`pg_hint_plan` 的核心机制就是通过这个 hook 注入被 hint 指定的路径。
### 2.5 对象访问 Hook
```c
// src/include/catalog/objectaccess.h
typedef void (*object_access_hook_type)
(ObjectAccessType access, Oid classId, Oid objectId,
int subId, void *arg);
extern PGDLLIMPORT object_access_hook_type object_access_hook;object_access_hook 覆盖所有 DDL
的对象访问——创建、修改、删除、搜索路径变更。sepgsql
用它校验每个 DDL 操作的 SELinux
权限。这是唯一不支持链式调用的 hook
之一——只有单指针,后注册的扩展覆盖前者。
2.6 认证 Hook
// src/include/libpq/auth.h
typedef void (*ClientAuthentication_hook_type)(Port *port, int status);
extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook;
```text
认证 hook——在标准密码认证后触发,允许扩展实现额外的认证步骤。同样不支持链式调用。
### 2.7 共享内存与日志 Hook
```c
// src/include/storage/shmem.h
extern PGDLLIMPORT shmem_startup_hook_type shmem_startup_hook;
// src/include/utils/elog.h
extern PGDLLIMPORT emit_log_hook_type emit_log_hook;shmem_startup_hook
是唯一能让扩展在共享内存中拥有持久数据结构的机制——但它必须在
shared_preload_libraries
中加载才能触发。pg_stat_statements 用它分配
query hash 的共享 hash table。emit_log_hook
在每条日志发射时触发,pgaudit
用它转发审计日志。
2.8 Hook 调用链与冲突
Hook 的典型调用模式:
// src/backend/optimizer/plan/planner.c
PlannedStmt *
planner(Query *parse, const char *query_string,
int cursorOptions, ParamListInfo boundParams)
{
if (planner_hook)
return (*planner_hook)(parse, query_string,
cursorOptions, boundParams);
return standard_planner(parse, query_string,
cursorOptions, boundParams);
}
```text
如果 `planner_hook` 为 `NULL`,走标准路径。如果扩展注册了 hook,由扩展决定是否调用 `standard_planner()`。典型扩展实现模式:
```c
static planner_hook_type prev_planner_hook = NULL;
static PlannedStmt *
my_planner(Query *parse, const char *query_string,
int cursorOptions, ParamListInfo boundParams)
{
// 1. 自己的逻辑
do_something(parse);
// 2. 调用原始 planner
if (prev_planner_hook)
return prev_planner_hook(parse, query_string,
cursorOptions, boundParams);
return standard_planner(parse, query_string,
cursorOptions, boundParams);
}
void _PG_init(void)
{
prev_planner_hook = planner_hook;
planner_hook = my_planner;
}保存 prev 是链式调用的关键。但
object_access_hook 和
ClientAuthentication_hook
不支持链式调用——如果两个扩展同时注册,后加载的覆盖前者。
三、FDW 的接口体系:FdwRoutine
FDW 没有使用全局 hook
指针,而是通过更结构化的接口:每个外部表在
pg_class 中关联一个
FOREIGN DATA WRAPPER,每个 FDW 通过 handler
函数返回 FdwRoutine
结构体。这种设计的优势是多个 FDW
可以共存且不互相干扰——回调是 per-table、per-query
隔离的。
3.1 FdwRoutine 的核心回调
// src/include/foreign/fdwapi.h, FdwRoutine(PG 17,简化摘录)
typedef struct FdwRoutine
{
NodeTag type;
/* 规划阶段回调 */
GetForeignRelSize_function GetForeignRelSize;
GetForeignPaths_function GetForeignPaths;
GetForeignPlan_function GetForeignPlan;
/* 扫描执行回调 */
BeginForeignScan_function BeginForeignScan;
IterateForeignScan_function IterateForeignScan;
EndForeignScan_function EndForeignScan;
ReScanForeignScan_function ReScanForeignScan;
/* 写操作回调 */
AddForeignUpdateTargets_function AddForeignUpdateTargets;
PlanForeignModify_function PlanForeignModify;
BeginForeignModify_function BeginForeignModify;
ExecForeignInsert_function ExecForeignInsert;
ExecForeignUpdate_function ExecForeignUpdate;
ExecForeignDelete_function ExecForeignDelete;
EndForeignModify_function EndForeignModify;
/* JOIN / ORDER BY / GROUP BY pushdown */
GetForeignJoinPaths_function GetForeignJoinPaths;
GetForeignUpperPaths_function GetForeignUpperPaths;
/* ANALYZE 支持 */
AnalyzeForeignTable_function AnalyzeForeignTable;
/* IMPORT FOREIGN SCHEMA */
ImportForeignSchema_function ImportForeignSchema;
/* EXPLAIN 输出 */
ExplainForeignScan_function ExplainForeignScan;
ExplainForeignModify_function ExplainForeignModify;
/* 并行查询支持 */
IsForeignScanParallelSafe_function IsForeignScanParallelSafe;
/* 异步执行(PG 14+) */
ForeignAsyncRequest_function ForeignAsyncRequest;
ForeignAsyncConfigureWait_function ForeignAsyncConfigureWait;
ForeignAsyncNotify_function ForeignAsyncNotify;
} FdwRoutine;
```text
handler 函数的典型实现:
```c
// contrib/postgres_fdw/postgres_fdw.c, postgres_fdw_handler()(简化)
Datum postgres_fdw_handler(PG_FUNCTION_ARGS)
{
FdwRoutine *routine = makeNode(FdwRoutine);
routine->GetForeignRelSize = postgresGetForeignRelSize;
routine->GetForeignPaths = postgresGetForeignPaths;
routine->GetForeignPlan = postgresGetForeignPlan;
routine->BeginForeignScan = postgresBeginForeignScan;
routine->IterateForeignScan = postgresIterateForeignScan;
routine->ReScanForeignScan = postgresReScanForeignScan;
routine->EndForeignScan = postgresEndForeignScan;
// ... write callbacks ...
PG_RETURN_POINTER(routine);
}3.2 三个规划回调:扩展如何影响 Planner
外部表的规划流程通过三个回调嵌入
make_one_rel() 中:
make_one_rel()
└→ set_base_rel_sizes() → 调用 GetForeignRelSize()
└→ set_base_rel_pathlists() → 调用 GetForeignPaths()
└→ create_plan() → 调用 GetForeignPlan()
GetForeignRelSize — 获取基本统计信息
// 回调签名
typedef void (*GetForeignRelSize_function)
(PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);
```text
PG 的 planner 调用此回调获取外部表的基本统计。FDW 在此设置 `baserel->rows`(预估行数)和 `baserel->width`(平均行宽)。postgres_fdw 向远端发送 `EXPLAIN (FORMAT JSON)` 获取行数估算:
```c
// contrib/postgres_fdw/postgres_fdw.c, postgresGetForeignRelSize()(简化)
void postgresGetForeignRelSize(PlannerInfo *root, RelOptInfo *baserel,
Oid foreigntableid)
{
fpinfo = get_fpinfo(root, baserel, foreigntableid);
// 向远端发送 EXPLAIN 获取行数估算
fpinfo->rows = get_remote_estimate(query_string, fpinfo->conn);
baserel->rows = fpinfo->rows;
baserel->tuples = fpinfo->rows;
}GetForeignPaths — 生成访问路径
typedef void (*GetForeignPaths_function)
(PlannerInfo *root, RelOptInfo *baserel, Oid foreigntableid);
```text
此回调为外部表生成一种或多种路径。最简单的 FDW 只生成一条 `ForeignPath`。postgres_fdw 则根据 pushdown 分析生成多个路径:参数化路径、排序路径等。
```c
// contrib/postgres_fdw/postgres_fdw.c, postgresGetForeignPaths()(简化)
void postgresGetForeignPaths(PlannerInfo *root, RelOptInfo *baserel,
Oid foreigntableid)
{
// 1. 分析哪些 WHERE 条件可 pushdown
classifyConditions(root, baserel, ...);
// 2. 构造 pushdown SQL,估算代价
estimate_path_cost_size(root, baserel, ...);
// 3. 创建 ForeignPath 节点,提交到 planner
path = create_foreignscan_path(root, baserel,
rows, startup_cost, total_cost,
pathkeys, ...);
add_path(baserel, (Path *) path);
// 4. 如果有参数化条件,也生成参数化路径
if (has_parameterized_condition)
create_parameterized_foreign_path(root, baserel, ...);
}Planner 将 ForeignPath
与其他候选路径(SeqScan、IndexScan
等)一同比较,按总代价选择最优者。
GetForeignPlan — 冻结计划节点
typedef ForeignScan *(*GetForeignPlan_function)
(PlannerInfo *root, RelOptInfo *baserel,
Oid foreigntableid, ForeignPath *best_path,
List *fdw_private);
```text
Planner 选中最优路径后,调用此回调生成 `ForeignScan` 计划节点。FDW 在此生成最终发送到远端的 SQL——这是 pushdown 的落地位置。
```c
// contrib/postgres_fdw/postgres_fdw.c, postgresGetForeignPlan()(简化)
ForeignScan *postgresGetForeignPlan(PlannerInfo *root, RelOptInfo *baserel, ...)
{
// 1. 最终确定远端 SQL(含确定的 pathkey / limit)
deparseFinalSelectSql(&sql, root, baserel, ...);
// 2. SQL 和连接信息存入 fdw_private
List *fdw_private = list_make3(
makeString(sql), // 远端 SQL
makeInteger(fpinfo->server), // server OID
makeNode(fpinfo->user) // user mapping
);
// 3. 创建 ForeignScan 节点
return make_foreignscan(targetlist, qual, baserel,
fdw_private, ...);
}四、postgres_fdw 的 pushdown 机制
postgres_fdw 不是把远端表”当成本地表的一个镜像”——它把 SQL 的某些部分(WHERE、JOIN、ORDER BY、LIMIT、GROUP BY、聚合)推到远端 PostgreSQL 执行,其余留在本地。
4.1 什么会被 pushdown
WHERE 子句 — 最基础的
pushdown。条件是:表达式中所有 Var
都来自同一个远程表,且无 VOLATILE 函数。
SELECT * FROM remote_t WHERE id = 1;
-- 远程 SQL:SELECT id, name FROM remote_t WHERE id = 1
```text
**ORDER BY + LIMIT** — 如果排序列在远程表上:
```sql
SELECT * FROM remote_t ORDER BY id LIMIT 10;
-- 远程 SQL:SELECT id, name FROM remote_t ORDER BY id LIMIT 10但如果 ORDER BY 涉及表达式(如
ORDER BY id + local_func())或排序字符集与本地不同,pushdown
失败。
JOIN(PG 11+) — 两个表在同一个远程 server 上时,整个 JOIN 可 pushdown:
SELECT * FROM remote_t1 JOIN remote_t2 ON remote_t1.id = remote_t2.id;
-- 远程 SQL:SELECT ... FROM remote_t1 JOIN remote_t2 ON remote_t1.id = remote_t2.id
```text
postgres_fdw 在 `postgresGetForeignJoinPaths` 中检查 JOIN 两侧的 server OID 和 user mapping 是否一致,以及 JOIN 条件是否可 deparse。
**GROUP BY / 聚合(PG 14+)** — 标准聚合函数可 pushdown:
```sql
SELECT avg(salary), dept FROM remote_t GROUP BY dept;
-- 远程 SQL:SELECT avg(salary), dept FROM remote_t GROUP BY dept支持 pushdown 的聚合包括
SUM、AVG、MIN、MAX、COUNT。不支持
GROUPING SETS/CUBE/ROLLUP
和自定义聚合函数。
4.2 什么不会被 pushdown
- 本地和远程表的混合 JOIN — 本地表和远程表的 JOIN 必须在本地执行 hash join 或 nestloop join。
- 包含
random()、current_date等 volatile 函数的 WHERE 条件 — 本地和远端的函数返回值不一致。 - FOR UPDATE / FOR SHARE — 除非设置
row-level locking,否则在本地通过fetch_size分批获取并锁定。 - 涉及本地用户自定义函数或本地表达式的条件。
4.3 pushdown 决策的源码路径
WHERE pushdown 的核心判断在
classifyConditions() 中:
// contrib/postgres_fdw/postgres_fdw.c, classifyConditions()(简化)
void classifyConditions(PlannerInfo *root, RelOptInfo *baserel, ...)
{
foreach(lc, baserel->baserestrictinfo)
{
RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
if (is_foreign_expr(root, baserel, ri->clause))
// 所有 Var 来自远程表且无 volatile 函数 → pushdown
fpinfo->remote_conds = lappend(fpinfo->remote_conds, ri);
else
// 含本地表达式 → 留在本地
fpinfo->local_conds = lappend(fpinfo->local_conds, ri);
}
}
```text
`is_foreign_expr()` 递归检查表达式树的每个节点:所有 `Var` 必须属于当前远程表,表达式中不能包含非 `IMMUTABLE` 函数,并且远程 PG 版本必须支持涉及的运算符。
SQL 构造在 `contrib/postgres_fdw/deparse.c` 中完成。`deparseSelectSqlForRel()` 将规划器内部的 `RestrictInfo` 列表翻译为远端 SQL 字符串,处理列名引用、表达式格式、类型转换等细节。
### 4.4 代价模型
postgres_fdw 的 pushdown 不是"能推就推"。`estimate_path_cost_size()` 比较两种方案的代价:
- **全量 fetch + 本地处理**:`baserel->rows × (cpu_tuple_cost + 网络传输开销)`
- **pushdown**:`远端执行代价 + 结果行数 × 网络传输开销`
当 `use_remote_estimate = true`(默认),postgres_fdw 向远端发 `EXPLAIN` 获取实际代价估算。`fetch_size`(默认 100)控制每次从远端拉取的行数——不是一次拉完整个结果集,而是用游标分批获取,避免大结果集撑爆本地内存。
---
## 五、实验:观察 pushdown 行为
### 5.1 环境准备
```sql
CREATE EXTENSION postgres_fdw;
CREATE SERVER remote_pg
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'remote_db');
CREATE USER MAPPING FOR CURRENT_USER
SERVER remote_pg
OPTIONS (user 'postgres');
IMPORT FOREIGN SCHEMA public FROM SERVER remote_pg INTO public;5.2 验证各类型 pushdown
-- 查看 WHERE pushdown
EXPLAIN (VERBOSE) SELECT * FROM remote_orders WHERE total > 500;
-- 输出含 Remote SQL: SELECT ... FROM public.orders WHERE ((total > 500))
-- 查看 ORDER BY + LIMIT pushdown
EXPLAIN (VERBOSE) SELECT * FROM remote_orders ORDER BY total LIMIT 10;
-- Remote SQL: SELECT ... FROM public.orders ORDER BY total ASC LIMIT 10
-- 查看 JOIN pushdown(两表在同一 remote server 上)
CREATE FOREIGN TABLE remote_users (
id INT,
name TEXT
) SERVER remote_pg OPTIONS (schema_name 'public', table_name 'users');
EXPLAIN (VERBOSE)
SELECT u.name, o.total
FROM remote_users u JOIN remote_orders o ON u.id = o.user_id;
-- 如果输出只有 1 个 Foreign Scan 节点,说明 JOIN 被 pushdown
-- 如果输出有 2 个 Foreign Scan + 本地 Hash Join,说明 pushdown 失败
```bash
### 5.3 主动阻断 pushdown
```sql
-- 用本地表达式阻断 WHERE pushdown
EXPLAIN (VERBOSE) SELECT * FROM remote_orders WHERE total + 0 > 500;
-- WHERE 出现在 Filter: 行而非 Remote SQL: 行
-- 全表数据被拉到本地再过滤这个技巧在生产中也很有用——当怀疑 pushdown 的 SQL 在远端执行效率不高时,可以用这种方式临时回退到本地处理对比。
5.4 用 auto_explain 捕获 FDW 查询计划
LOAD 'auto_explain';
SET auto_explain.log_min_duration = 0;
SET auto_explain.log_analyze = on;
SET auto_explain.log_buffers = on;
SET auto_explain.log_verbose = on;
-- 执行 FDW 查询,在 PG 日志中查看完整计划
SELECT * FROM remote_orders WHERE total > 500;
```text
---
## 六、FDW 的坑和排查
### 6.1 pushdown 意外失败
最常见的原因是 WHERE 子句混入了本地函数或表达式。判断方法:检查 `EXPLAIN (VERBOSE)` 输出——如果条件出现在 `Filter:` 行而非 `Remote SQL:` 行,说明 pushdown 失败。
**排查脚本**:
```sql
-- 查看当前所有涉及 FDW 的活跃查询
SELECT pid, query, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE query LIKE '%remote_%' AND state = 'active';6.2 统计信息过期
远端统计信息变化后,本地 planner 不知道。postgres_fdw 的
use_remote_estimate 选项解决此问题:
ALTER SERVER remote_pg OPTIONS (ADD use_remote_estimate 'true');
```text
开启后,每次规划时间向远端发 `EXPLAIN` 获取最新行数估算。代价是每次规划增加一次远端查询。对规划频繁的 OLTP 场景需要评估开销。
### 6.3 fetch_size 的陷阱
```sql
ALTER FOREIGN TABLE remote_orders OPTIONS (SET fetch_size '1000');fetch_size 默认 100——即每次从远端拉 100
行。对于大结果集,这导致大量网络往返(延迟 ×
往返次数)。设得太高(如
50000)又会增加本地内存使用。最佳值取决于每次 fetch
的数据量和网络延迟。
6.4 join_collapse_limit 对 FDW JOIN pushdown 的影响
join_collapse_limit 控制 planner 展开显式
JOIN 的边界。如果设为 1,所有显式 JOIN 顺序被冻结,可能阻止
FDW 将两个远程表 JOIN
pushdown。默认值(8)通常足够,但如果你用了一堆显式
JOIN 且设了较低的
join_collapse_limit,可能意外禁用 FDW JOIN
pushdown。
6.5 事务隔离的不一致
postgres_fdw 在远端执行的操作从 PG 12 起在
REPEATABLE READ 下运行。如果本地是
SERIALIZABLE,远端也需升级——但这不是自动的。跨节点分布式事务的隔离语义是
FDW 作为 SQL/MED 标准实现的内在局限。
七、关键要点
- PG 的扩展系统有两个互不重叠的入口:全局
hook(
planner_hook等约 30 个函数指针,覆盖 planner/executor/utility/认证/对象访问七个子系统)和 FDW 的FdwRoutine回调接口(30+ 个回调函数指针,由 handler function 返回)。前者在_PG_init()注册,后者在CREATE FOREIGN TABLE时绑定到表。 - Hook
调用链无内置多扩展支持——扩展必须自行保存
prev指针并在自己的包裹函数中调用它。object_access_hook和ClientAuthentication_hook不支持链式调用。 - FDW 的三个核心规划回调嵌入
make_one_rel()标准流程——GetForeignRelSize(估算行数)→GetForeignPaths(生成路径并参加代价比较)→GetForeignPlan(冻结 SQL)。FDW 通过这些回调获得 planner 的完整控制权。 - postgres_fdw 的 pushdown 依赖
is_foreign_expr()的判定——只有所有Var属于远程表且无 volatile 函数的条件能推到远端。JOIN、ORDER BY、LIMIT、GROUP BY、简单聚合都可 pushdown,但本地/远程混合 JOIN、本地表达式和 volatile 函数会阻断 pushdown。 EXPLAIN (VERBOSE)是调试 pushdown 的第一工具——Remote SQL:行告诉你远端实际执行的 SQL,Filter:行告诉你本地过滤了哪些条件。use_remote_estimate解决统计信息过期问题。
参考资料
源码(PG 17)
src/include/foreign/fdwapi.h:FdwRoutine结构体定义,所有回调函数签名src/backend/foreign/foreign.c:GetFdwRoutineByRelId()、GetForeignServer()、GetUserMapping()src/backend/optimizer/path/allpaths.c:set_foreign_size()、set_foreign_pathlist()— 调用 FDW 规划回调src/backend/optimizer/plan/createplan.c:create_foreignscan_plan()— 创建 ForeignScan 计划节点src/backend/executor/nodeForeignscan.c:ExecInitForeignScan()、ExecForeignScan()— ForeignScan 执行器src/backend/commands/extension.c:CreateExtension()、read_extension_control_file()contrib/postgres_fdw/postgres_fdw.c:postgres_fdw_handler()、postgresGetForeignRelSize()、postgresGetForeignPaths()、postgresGetForeignPlan()、classifyConditions()contrib/postgres_fdw/deparse.c:deparseSelectSqlForRel()、is_foreign_expr()— SQL 构造与 pushdown 判定contrib/postgres_fdw/connection.c:GetConnection()— 远端连接管理src/include/optimizer/planner.h:planner_hook声明src/include/executor/executor.h:ExecutorStart/Run/Finish/End_hook声明src/include/tcop/utility.h:ProcessUtility_hook声明src/include/optimizer/paths.h:set_rel_pathlist_hook、set_join_pathlist_hook声明src/include/catalog/objectaccess.h:object_access_hook声明src/include/libpq/auth.h:ClientAuthentication_hook声明src/include/utils/elog.h:emit_log_hook声明
官方文档
- PostgreSQL Documentation, Chapter 59: Writing a Foreign Data Wrapper
- PostgreSQL Documentation, Chapter 5.11: Foreign Data
- PostgreSQL Documentation, Section 50.5: Planner/Optimizer — 扩展对规划的介入
- PostgreSQL Documentation, Appendix F: Additional Supplied Modules — postgres_fdw
论文/标准
- ISO/IEC 9075-9:2016 (SQL/MED — Management of External Data) — FDW 的 SQL 标准基础
- Melton, J. et al. SQL/MED — A Status Report. SIGMOD Record, 2002.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【PG 内核】执行器与表达式求值:从计划树到行数据的一趟流水
拆解 PostgreSQL 执行器的火山模型(ExecInitNode→ExecProcNode→ExecEndNode)、Hash Join 内存化实现、EEO 表达式求值的 opcode 编译与解释执行机制、TupleTableSlot 的三种数据承载方式(virtual/heap/minimal)。附带查询 hang 住的完整诊断路径:pg_stat_activity 的 wait_event + pg_blocking_pids() 追踪锁等待链 + EXPLAIN ANALYZE 计划行数与实际行数差异定位。
【PG 内核】进程模型与共享内存:Postmaster 如何管理 100 个 Backend
拆解 PostgreSQL 多进程架构的核心:Postmaster 的启动与信号处理、Backend 进程的 fork()→InitPostgres→主循环生命周期、CreateSharedMemoryAndSemaphores() 的共享内存初始化流程、PGPROC/ProcArray/PGXACT 等关键共享内存结构的内存布局,以及 Background Worker 的注册与调度。理解了这个地基,才能理解 PG 为什么用进程而不是线程,以及 max_connections 为什么不能随便调大。
【PG 内核】页面布局与元组格式:PG 如何把一行数据塞进 8KB
拆解 PostgreSQL 的物理存储层:Page 的 8KB 布局(PageHeaderData、ItemId 数组、special space)、HeapTupleHeaderData 的字段语义(xmin/xmax/ctid/t_infomask/t_infomask2)、TOAST 外存机制的压缩阈值与四种策略(PLAIN/EXTENDED/EXTERNAL/MAIN),以及用 pageinspect 扩展直接观察页面字节。理解页面格式是理解 VACUUM、Index Scan、MVCC 可见性判断的共同前提。
【PG 内核】MVCC 实现:CLOG、hint bit 与快照可扩展性
在已有 MVCC 文章基础上深入 PG 并发控制的三个基础设施:CLOG 的 SLRU 结构(事务状态位、页面格式、SLRU 淘汰)、hint bit 的写入时机和竞争问题(何时写、谁写、写坏了怎么办)、PG 14 snapshot scalability 优化的具体机制(ProcArrayLock 为什么是瓶颈、xid/xmin 的原子更新如何减少持锁路径),以及事务 ID 回卷(wraparound)的威胁模型。最后与 InnoDB undo log 方案做系统性对比。