PostgreSQL 外部数据包装器(FDW)实战

系统讲解 PostgreSQL FDW:fdw_handler 与 FdwRoutine 回调架构、postgres_fdw 的 CREATE SERVER 与 USER MAPPING、IMPORT FOREIGN SCHEMA、use_remote_estimate 与 fetch_size 与 async_capable 选项、JOIN 与 ORDER BY 与聚合的下推条件、file_fdw 读 CSV 与日志、dblink 对比、远程事务与两阶段提交、批量写入与网络往返调优。

当数据分散在多个 PostgreSQL 实例、MySQL、Oracle、CSV 文件甚至对象存储里时,ETL 搬运是最笨的解法。FDW(Foreign Data Wrapper,外部数据包装器)让远程表在本地看起来就是一张普通表——你可以 SELECT、JOIN、INSERT,规划器会尽量把工作推给远端执行。这套机制自 SQL/MED 标准演化而来,是现代 PostgreSQL 做数据联邦的核心工具。

核心认知:FDW 的性能上限由「下推能力」决定。凡是不能推给远端的工作,都会变成把远端数据拉到本地再算,网络往返与数据量是主要成本。


一、FDW 架构

1.1 fdw_handler 与 FdwRoutine

FDW 本质是一个共享库,导出名为 fdw_handler 的函数,返回一个填充好的 FdwRoutine 结构体指针。PostgreSQL 核心在规划与执行时按需调用其中的回调。

/* src/include/foreign/fdwapi.h,节选 */
typedef struct FdwRoutine
{
    NodeTag     type;
    GetForeignRelSize_function GetForeignRelSize;  /* 估算行数 */
    GetForeignPaths_function   GetForeignPaths;    /* 生成访问路径 */
    GetForeignPlan_function    GetForeignPlan;     /* 生成计划节点 */
    BeginForeignScan_function  BeginForeignScan;
    IterateForeignScan_function IterateForeignScan; /* 逐行取数 */
    EndForeignScan_function    EndForeignScan;
    ExecForeignInsert_function ExecForeignInsert;   /* 写操作,可选 */
    GetForeignJoinPaths_function  GetForeignJoinPaths;  /* JOIN 下推,可选 */
    GetForeignUpperPaths_function GetForeignUpperPaths; /* 聚合下推,可选 */
} FdwRoutine;
SELECT fdwname, fdwhandler::regproc, fdwvalidator::regproc
FROM pg_foreign_data_wrapper;   -- 查看已安装的 FDW

1.2 规划器与执行器的交互

1. 规划器遇到外表 -> GetForeignRelSize 估算行数
2. GetForeignPaths 生成候选路径(含代价)
3. 若 FDW 支持 -> GetForeignJoinPaths / GetForeignUpperPaths 生成下推路径
4. 选中路径 -> GetForeignPlan 生成 ForeignScan 节点,携带要发给远端的 SQL
5. 执行 -> BeginForeignScan 建连接,IterateForeignScan 逐行取回

1.3 关键回调函数

回调作用缺失后果
GetForeignRelSize估算远端表行数无法规划
GetForeignPaths生成全表扫描路径无法规划
GetForeignJoinPaths支持 JOIN 下推JOIN 在本地做
GetForeignUpperPaths支持聚合与排序下推聚合排序在本地做
ExecForeignInsert支持 INSERT外表只读

二、postgres_fdw 实战

2.1 创建 server 与 user mapping

-- 1. 安装扩展
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

-- 2. 定义远端服务器
CREATE SERVER remote_pg
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.0.21', port '5432', dbname 'warehouse',
             fetch_size '10000', async_capable 'true');

-- 3. 建立用户映射(本地用户 -> 远端用户)
CREATE USER MAPPING FOR app_user
    SERVER remote_pg OPTIONS (user 'remote_ro', password 'secret');

-- 4. 创建外表(可手工建,也可 IMPORT)
CREATE FOREIGN TABLE orders_remote (
    id bigint, customer_id bigint, amount numeric(12,2), created_at timestamptz
) SERVER remote_pg OPTIONS (schema_name 'public', table_name 'orders');

-- 5. 验证连通性
SELECT count(*) FROM orders_remote;

CREATE SERVER 与 USER MAPPING 的选项是连接级的,CREATE FOREIGN TABLE 的选项是表级的。连接级选项可用 ALTER SERVER ... OPTIONS (SET host '...') 修改。

2.2 IMPORT FOREIGN SCHEMA

手工建外表在大 schema 下不现实,IMPORT FOREIGN SCHEMA 一次导入远端整个 schema 的表定义。

-- 导入全部表
IMPORT FOREIGN SCHEMA public
    FROM SERVER remote_pg INTO local_schema;

-- 只导入部分表
IMPORT FOREIGN SCHEMA public
    LIMIT TO (orders, customers, products)
    FROM SERVER remote_pg INTO local_schema;

-- 排除部分表
IMPORT FOREIGN SCHEMA public
    EXCEPT (temp_logs, audit_trail)
    FROM SERVER remote_pg INTO local_schema;

需要先创建目标 schema:CREATE SCHEMA local_schema;。导入不会复制数据,只是建立元数据映射。

2.3 关键选项

-- 服务器级
ALTER SERVER remote_pg OPTIONS (
    SET use_remote_estimate 'true',      -- 让远端 EXPLAIN 提供真实代价
    SET fetch_size '5000',               -- 每次批量取回的行数
    SET async_capable 'true',            -- 允许异步并行取数
    SET extensions 'postgis',            -- 假设远端已装扩展,函数可下推
    SET updatable 'true'                 -- 允许写操作
);

-- 表级覆盖
ALTER FOREIGN TABLE orders_remote OPTIONS (
    SET use_remote_estimate 'true',
    SET fetch_size '20000'
);

use_remote_estimate = true 会让规划时对远端发 EXPLAIN,代价更准但增加往返;表多或网络慢时应权衡。fetch_size 决定一次 FETCH 取多少行,太小则往返多,太大则内存占用高。用 SET postgres_fdw.application_name = 'reporting_job' 可给远端连接打标签,便于远端审计与问题定位。


三、下推能力与限制

3.1 可下推的算子

postgres_fdw 能把以下操作编译进发给远端的 SQL:

- WHERE 过滤(含大部分内置函数与操作符)
- JOIN(内连接、左外、右外、全外,需满足可下推条件)
- ORDER BY(当远端排序代价更低时)
- LIMIT / OFFSET
- 聚合(GROUP BY、COUNT、SUM、AVG 等)
- 部分子查询与 CTE(视版本与条件)

3.2 不可下推与本地过滤

以下情况会导致过滤或计算在本地进行:

- 使用了远端不存在的函数或扩展
- 类型转换依赖本地定义的自定义类型
- 混合了本地表与远端表的 JOIN(只有整棵子树都是远端才能下推)
-- 观察是否下推:看 Foreign Scan 的 Remote SQL
EXPLAIN (VERBOSE, COSTS OFF)
SELECT customer_id, count(*) FROM orders_remote
WHERE created_at > '2026-01-01' GROUP BY customer_id;

3.3 EXPLAIN 观察远程 SQL

EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT o.id, c.name FROM orders_remote o
JOIN customers_remote c ON c.id = o.customer_id
WHERE o.amount > 1000;
下推良好: Foreign Scan on (o JOIN c)  -> Remote SQL: SELECT ... FROM orders o JOIN customers c ...
未下推:  Hash Join
            -> Foreign Scan on orders_remote
            -> Foreign Scan on customers_remote

若计划里出现 Foreign Scan on <join result> 且 Remote SQL 含 JOIN,说明 JOIN 已下推;若看到本地 Hash Join 加两个 Foreign Scan,说明两表数据都被拉到本地,网络成本高。


四、file_fdw 读取外部文件

file_fdw 把服务器上的 CSV 或文本文件当成表读,常用于导入日志或临时数据。

CREATE EXTENSION IF NOT EXISTS file_fdw;

CREATE SERVER file_srv FOREIGN DATA WRAPPER file_fdw;

CREATE FOREIGN TABLE access_log (
    ts        timestamptz,
    ip        inet,
    method    text,
    path      text,
    status    int,
    bytes     bigint
) SERVER file_srv
OPTIONS (filename '/var/log/nginx/access.csv', format 'csv', header 'true');

常用选项:filename(文件绝对路径,仅超级用户可指定)、program(执行的命令,取其标准输出,如 'gzip -dc /path/a.csv.gz')、format(csv / text / binary)、header(true 表示首行是列名)、delimiter、null。

-- 直接统计日志,无需先 COPY 入库
SELECT status, count(*) FROM access_log GROUP BY status ORDER BY 2 DESC;

file_fdw 是只读的,且 filename 只能由超级用户设置——这是安全边界,防止普通用户读取任意文件。


dblink 是更老的跨库访问工具,它不做「表映射」,而是执行一段 SQL 字符串并返回结果。

CREATE EXTENSION IF NOT EXISTS dblink;

SELECT * FROM dblink(
    'host=10.0.0.21 dbname=warehouse user=remote_ro password=secret',
    'SELECT id, amount FROM orders WHERE amount > 1000'
) AS t(id bigint, amount numeric);
维度postgres_fdwdblink
使用方式像本地表一样 SELECT/JOIN手写 SQL 字符串
下推优化规划器自动下推无,全靠手写
事务支持两阶段提交需手动 dblink_exec 管理
连接管理连接池式复用每次调用建立连接
适用场景长期联邦、复杂 JOIN一次性取数、动态 SQL

结论:长期集成用 FDW,临时取数或需要动态 SQL 时用 dblink。


六、其他 FDW 生态

mysql_fdw       访问 MySQL/MariaDB,支持读写与条件下推
oracle_fdw      通过 OCI 访问 Oracle,企业迁移常用
sqlite_fdw      访问 SQLite 文件,适合嵌入式数据整合
mongo_fdw       访问 MongoDB 集合
parquet_s3_fdw  直接读 S3 上的 Parquet

选择原则:先确认该 FDW 是否支持你的操作类型(只读/可写)与下推能力,再评估维护活跃度。跨异构库的 FDW 通常下推能力弱,性能远不如同构的 postgres_fdw。


七、写操作与事务

7.1 远程事务

-- 需要显式开启可写
ALTER FOREIGN TABLE orders_remote OPTIONS (ADD updatable 'true');

INSERT INTO orders_remote (id, customer_id, amount)
VALUES (1001, 42, 199.00);

UPDATE orders_remote SET amount = amount * 1.1 WHERE id = 1001;
DELETE FROM orders_remote WHERE id = 1001;

本地事务与远程事务是分离的:本地 BEGIN 不会自动开启远程事务,除非启用两阶段提交。默认情况下,每条语句在远端独立提交。

7.2 两阶段提交

要保证本地与远端原子提交,需要两端都开启 max_prepared_transactions。

-- 两端配置(需要重启)
ALTER SYSTEM SET max_prepared_transactions = 100;

-- 本地事务中修改远端表
BEGIN;
UPDATE orders_remote SET amount = 100 WHERE id = 1;
UPDATE local_audit SET note = 'adjusted' WHERE id = 1;
COMMIT;   -- postgres_fdw 会用 PREPARE TRANSACTION 保证两端一致

代价是每条跨库事务都要两次额外的网络往返(PREPARE 与 COMMIT PREPARED),吞吐下降明显。只在真正需要原子性时启用。

7.3 批量写入

-- 大批量写入用 COPY 语法,postgres_fdw 会走批处理路径
COPY orders_remote (id, customer_id, amount) FROM STDIN WITH (FORMAT csv);

-- 提高批大小(默认 1,即逐行 ExecForeignInsert,每行一次往返)
ALTER FOREIGN TABLE orders_remote OPTIONS (ADD batch_size '1000');

八、性能调优与常见坑

8.1 网络往返是主要成本

优化方向:提高 fetch_size 减少 SELECT 往返、提高 batch_size 减少 INSERT 往返、让过滤与 JOIN 与聚合尽量下推以减少传输行数、在远端建好索引让下推的 WHERE 走索引、用 async_capable 让多个 Foreign Scan 并行取数。

8.2 常见坑

1. 本地过滤:WHERE 用了远端没有的函数,导致全表拉回本地
2. 远程排序失效:ORDER BY 未下推,远端返回全量后本地排序
3. 连接数暴涨:每个本地会话占用一个远端连接,易打满远端 max_connections
4. 忘记 ANALYZE:本地无统计信息,规划器估算失准
5. 大事务跨库:两阶段提交期间远端持锁,易阻塞
6. 循环依赖:A 订阅 B、B 订阅 A,规划器可能递归
-- 让本地也持有统计信息,改善规划
ANALYZE orders_remote;

-- 监控远端连接占用
SELECT count(*) FROM pg_stat_activity WHERE application_name LIKE '%fdw%';

常见问题(FAQ)

FDW 查询为什么比本地表慢很多

因为存在网络往返与数据序列化开销。如果下推不完整,远端要把大量行传给本地再过滤,网络成为瓶颈。诊断方法是用 EXPLAIN (VERBOSE) 看 Remote SQL 是否包含预期的 WHERE、JOIN、GROUP BY;若没有,说明算子没下推,需要检查是否用了远端不支持的函数或类型。

use_remote_estimate 该开还是关

表少、网络快时开启更准,因为远端能给出真实的行数与代价。表多、网络慢或远端负载敏感时应关闭,改用本地 ANALYZE 得到的统计信息。折中方案是对关键大表单独开启,其余关闭。

postgres_fdw 支持跨库事务吗

支持,但需要两端都设置 max_prepared_transactions > 0,且提交时使用两阶段提交协议。代价是额外往返与远端持锁时间变长。若业务能接受最终一致,建议避免跨库强事务,改用幂等重试或补偿。

外表上能建索引吗

不能。外表没有本地存储,CREATE INDEX 会被拒绝。索引必须建在远端表上,通过 Remote SQL 下推的 WHERE 才能利用它。本地 ANALYZE 只影响规划估算,不改变远端执行。

为什么 IMPORT FOREIGN SCHEMA 后查询报列不存在

因为导入只复制当时的表结构快照。远端后来新增了列,本地外表不会自动同步,需要 ALTER FOREIGN TABLE ... ADD COLUMN 或重新导入。同理,远端删列会导致本地查询报错。建议在远端 DDL 变更后重新执行导入。


相关阅读

延伸阅读


完整示例(一键复制)

-- ========== 1. 安装并创建 server ==========
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

CREATE SERVER remote_pg
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.0.21', port '5432', dbname 'warehouse',
             fetch_size '10000', async_capable 'true');

CREATE USER MAPPING FOR app_user
    SERVER remote_pg OPTIONS (user 'remote_ro', password 'secret');

-- ========== 2. 导入远端 schema ==========
CREATE SCHEMA IF NOT EXISTS remote;
IMPORT FOREIGN SCHEMA public
    LIMIT TO (orders, customers, products)
    FROM SERVER remote_pg INTO remote;

-- ========== 3. 让远端提供真实代价并验证下推 ==========
ALTER SERVER remote_pg OPTIONS (SET use_remote_estimate 'true');
ANALYZE remote.orders;

EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT o.id, c.name, sum(o.amount) AS total
FROM remote.orders o
JOIN remote.customers c ON c.id = o.customer_id
WHERE o.created_at > '2026-01-01'
GROUP BY o.id, c.name ORDER BY total DESC LIMIT 20;

-- ========== 5. 写操作与两阶段提交 ==========
ALTER FOREIGN TABLE remote.orders OPTIONS (ADD updatable 'true', ADD batch_size '1000');
INSERT INTO remote.orders (id, customer_id, amount, created_at)
VALUES (2001, 42, 99.00, now());

-- 需两端 max_prepared_transactions > 0
BEGIN;
UPDATE remote.orders SET amount = 100 WHERE id = 2001;
UPDATE local_audit SET note = 'adjusted' WHERE id = 2001;
COMMIT;

-- ========== 6. 连接占用监控 ==========
SELECT count(*) AS fdw_conns
FROM pg_stat_activity WHERE application_name LIKE '%fdw%';

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. PostgreSQL 锁与阻塞分析
  2. COPY 与批量数据加载优化
  3. pgvector 向量检索与混合查询