当数据分散在多个 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 与 FDW 的对比
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_fdw | dblink |
|---|---|---|
| 使用方式 | 像本地表一样 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 变更后重新执行导入。
相关阅读
- PostgreSQL 逻辑复制 — 另一种跨库数据同步方式
- PostgreSQL 迁移指南 — 从其他数据库迁移数据
- PostgreSQL 事务、隔离级别与锁 — 两阶段提交与分布式事务
- PostgreSQL 扩展生态 — 扩展管理与分布式方案
- PostgreSQL 查询优化实战 — 用 EXPLAIN 分析下推效果
- PostgreSQL 专题导航
延伸阅读
- PostgreSQL 高级 SQL 查询实战 — JOIN 与聚合的下推前提
- PostgreSQL 监控与诊断体系 — 远程连接与等待事件监控
完整示例(一键复制)
-- ========== 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%';
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。