Skip to content

外部数据包装器(Foreign Data Wrapper) ​

PostgreSQL 里有一句流传很广的说法:"一切皆可成为一张表"。你可以在 PostgreSQL 中查询 MySQL 里的订单、读取服务器上的 CSV 日志、访问远程的另一台 PostgreSQL 实例,甚至去调用一个 HTTP 接口——而这些"外部数据"在 SQL 层面看起来就是一张普通的表。

实现这一能力的机制叫做外部数据包装器(Foreign Data Wrapper,简称 FDW)。它是 PostgreSQL 对 SQL 标准中 SQL/MED(Management of External Data,外部数据管理) 规范的实现:用一套统一的元数据描述"外部数据在哪里、怎么连、叫什么名字",然后由各个 FDW 插件把 SQL 操作翻译成对底层数据源的访问。

本节是全组文章的总览,重点讲清 FDW 的五层对象模型、一次跨库查询的执行流程,以及什么时候该用它、什么时候不该用它。后续小节会分别展开 postgres_fdw、file_fdw、第三方 FDW 和性能调优。

前置知识

阅读本节前,建议先了解:中文全文搜索配置、全文索引

基础概念 ​

为什么需要 FDW ​

在实际系统里,"数据都放在一个 PostgreSQL 实例里"往往只是理想状态。更常见的情况是:

  • 老的业务系统还在 MySQL 上跑,新系统用 PostgreSQL,两边数据需要联合查询;
  • 归档数据被导出成 CSV 文件,需要一个能直接用 SQL 查询的方式;
  • 数仓与业务库分属两个 PostgreSQL 集群,做报表时要 JOIN;
  • 服务器访问日志被 log_destination = 'csvlog' 落成文件,运维想直接写 SQL 分析。

传统解决办法有三种,各有明显缺点:

方案做法缺点
ETL 搬运定时把外部数据同步进本地表延迟高、存储翻倍、Schema 变更要改同步脚本
应用层拼接应用分别查两边,在代码里做 JOIN无法下推、无法分页、应用复杂度爆炸
dblink 函数调用在 SQL 里写 dblink('...', 'select ...')字符串拼 SQL、无类型检查、无执行计划优化

FDW 相当于第四种方案:把外部数据源纳管成表,让优化器、执行器、权限系统都能像对待本地表一样对待它。你写的是标准 SQL,FDW 负责翻译。

FDW 不是什么 ​

初学 FDW 最容易混淆的两个点,必须先澄清:

  • FDW 与 FOREIGN KEY(外键)无关。外键是"同一库内两张表之间的约束";FDW 是"把库外的数据当成表"。两者名字里都有 FOREIGN,但完全是两回事。
  • FDW 不是数据副本。外部表(foreign table)本身不存储数据,它只是一份元数据(元数据可以理解为"描述数据的数据",即表的定义),真正的数据仍在远端或文件里。每次查询都会实时去取(materialized 之类的能力要靠具体 FDW 或物化视图自己实现)。

五层对象模型 ​

FDW 的一切操作都围绕下面五个层级的对象展开。理解这五层,是理解全部 FDW 语法的基础:

层对象中文名职责
1FOREIGN DATA WRAPPER外部数据包装器定义"这类数据源怎么访问",由扩展提供,比如 postgres_fdw
2SERVER外部服务器定义一个具体的远端连接目标,比如"生产库 10.0.0.5:5432"
3USER MAPPING用户映射定义"本地某个角色连接该服务器时用什么凭据"
4FOREIGN TABLE外部表定义远端某张表/某个文件映射成本地哪张表、有哪些列
5查询—SELECT/INSERT/UPDATE/DELETE,由 FDW 翻译并下发

这五层是从"抽象"到"具体"的层层收窄。用一句大白话概括:

text
FOREIGN DATA WRAPPER   →  "我会开这种车"        (能力,由插件提供)
        │
        ▼
SERVER                 →  "我要去这个地址"      (目标,连接信息)
        │
        ▼
USER MAPPING           →  "我用这个账号登录"    (凭据)
        │
        ▼
FOREIGN TABLE          →  "我要读这张表"        (映射,列定义)
        │
        ▼
SELECT / INSERT ...    →  "开始干活"            (查询)

一次跨库查询的执行流程 ​

当你在本地执行一条涉及外部表的 SQL 时,内部大致经历以下步骤:

text
客户端
  │  1. SELECT * FROM remote_orders WHERE amount > 100;
  ▼
本地解析器 & 优化器
  │  2. 生成计划:判断哪些条件可以"下推"到远端
  ▼
FDW 的 Planner 回调
  │  3. 构造要发给远端的 SQL(可能带 WHERE / JOIN / LIMIT)
  ▼
FDW 的 Executor 回调
  │  4. 打开远端连接(复用连接缓存)
  │  5. 发送远程 SQL,按 fetch_size 分批取回结果
  ▼
本地执行器
  │  6. 对远端返回的行做本地再加工(如本地过滤、JOIN、排序)
  ▼
返回客户端

其中第 2、3 步的"下推"(pushdown)是 FDW 性能的核心话题:能下推的部分越多,网络传输和本地处理的数据就越少。我们会在 FDW 性能优化 中专门讨论。

常用系统目录与视图 ​

FDW 的对象信息存放在固定的系统目录中,排查问题时非常有用了:

视图 / 目录用途关键字段
pg_foreign_data_wrapper已安装的 FDWfdwname、fdwhandler、fdwvalidator
pg_foreign_server外部服务器srvname、srvowner、srvfdw、srvoptions
pg_user_mappings用户映射(普通用户可见脱敏信息)umuser、usename、srvname、umoptions
pg_foreign_table外部表ftrelid、ftserver、ftoptions
information_schema.foreign_tables外部表的标准视图foreign_table_schema、foreign_table_name、foreign_server_name
information_schema.foreign_table_options外部表选项foreign_table_name、option_name、option_value

注意:pg_user_mappings 与 pg_user_mapping 是两个不同的对象。前者是视图,普通用户也能查(密码会显示为 ****);后者是底层系统表,只有超级用户能访问。

语法与用法 ​

FDW 的建表流程是一套固定的"四步曲"。下面按顺序给出最精简的语法骨架,具体选项留待各分节展开。

第一步:安装 FDW 扩展 ​

sql
-- postgres_fdw 是随 PostgreSQL 一起发布的"contrib"扩展,需要显式安装
CREATE EXTENSION postgres_fdw;

-- 查看当前库已经装了哪些 FDW
SELECT fdwname, fdwhandler::regproc, fdwvalidator::regproc
FROM pg_foreign_data_wrapper
ORDER BY fdwname;

CREATE EXTENSION 会在当前数据库中注册这个 FDW。注意它的作用范围是数据库级的:同一个集群里的另一个数据库需要单独再装一次。

第二步:创建外部服务器 ​

sql
CREATE SERVER remote_pg
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (
        host '10.0.0.5',
        port '5432',
        dbname 'production'
    );

SERVER 承载的是"连到哪里"的信息。命名建议带上用途前缀(如 remote_pg、legacy_mysql),避免和本地对象混淆。

第三步:创建用户映射 ​

sql
-- 只给当前角色建映射(最常见的做法)
CREATE USER MAPPING FOR CURRENT_USER
    SERVER remote_pg
    OPTIONS (user 'readonly_app', password 'secret');

-- 也可以给 PUBLIC 建一份默认映射,所有角色共用
CREATE USER MAPPING FOR PUBLIC
    SERVER remote_pg
    OPTIONS (user 'readonly_app', password 'secret');

USER MAPPING 解决"用谁的身份去连远端"的问题。匹配顺序是:先找当前角色自己的映射,找不到再用 PUBLIC 的映射。

第四步:创建外部表 ​

sql
-- 手写列定义
CREATE FOREIGN TABLE remote_orders (
    id          bigint,
    tenant_id   bigint,
    amount      numeric(12,2),
    created_at  timestamptz
)
SERVER remote_pg
OPTIONS (schema_name 'public', table_name 'orders');

-- 或者批量导入远端某个 schema 下的所有表
IMPORT FOREIGN SCHEMA public
    LIMIT TO (orders, order_items)
    FROM SERVER remote_pg
    INTO public;

手写外部表时列必须与远端一一对应,import foreign schema 则能自动生成列定义,是更省事也更容易保持同步的方式。

查询与清理 ​

sql
-- 像普通表一样查询
SELECT tenant_id, count(*), sum(amount)
FROM remote_orders
WHERE created_at >= now() - interval '7 days'
GROUP BY tenant_id
ORDER BY sum(amount) DESC;

-- 清理:顺序与创建相反
DROP FOREIGN TABLE remote_orders;
DROP USER MAPPING FOR CURRENT_USER SERVER remote_pg;
DROP SERVER remote_pg;
DROP EXTENSION postgres_fdw;

删除时要注意依赖关系:外部表依赖 server,server 依赖 extension。用 DROP ... CASCADE 可以一次性清理,但生产环境建议逐层显式删除,避免误删。

详细说明 ​

内置的 FDW 有哪些 ​

PostgreSQL 发行版自带两个 FDW 实现,它们都在 contrib 里,需要 CREATE EXTENSION 激活:

  • postgres_fdw:访问远程 PostgreSQL。功能最完整,支持读写、事务、下推、连接池式缓存。
  • file_fdw:把服务器上的文本文件(CSV、TSV 等)当成只读外部表。它复用了 COPY FROM 的解析机制。

除此之外,社区提供了大量第三方 FDW,比如访问 MySQL、Oracle、SQLite、MongoDB、Redis 的封装。选型时最重要的判断标准是:是否还在维护、是否支持你需要的下推能力。

FDW 的分层设计带来了什么好处 ​

五层模型不只是"层层配置比较清晰",它有实实在在的工程价值:

  • 连接信息与表定义解耦:同一个 server 下可以挂成百上千张外部表,换 IP 时只改 server 一处。
  • 凭据与对象定义解耦:USER MAPPING 让"谁在连"和"连到哪"分开管理,可以把密码集中放在映射里而不污染表定义。
  • 权限可以逐层授予:USAGE on server、USAGE on FDW、SELECT on foreign table,三处独立控制。
  • 一个 FDW 支撑多数据源:同一类型的多个数据源建多个 server 即可。

FDW 与本地表的差异 ​

外部表"看起来像表",但有几处行为与本地表不同,写业务代码时必须心里有数:

特性本地表外部表
数据存储存在本地不存在本地,实时去取
约束(PRIMARY KEY / CHECK)被强制不被强制,仅"假定为真"
索引可建不能建(由远程/远端自己保证)
触发器支持不支持(由 FDW 决定,核心层不支持)
TRUNCATE支持由 FDW 决定(postgres_fdw 支持)
统计信息ANALYZE 收集需要能 ANALYZE,否则计划可能失准
事务本地事务跨库需两阶段提交(2PC)配合

关于"约束不被强制"这一点值得展开:外部表上你可以声明 PRIMARY KEY、NOT NULL、CHECK,但 PostgreSQL 不会去远端验证它们,只是把它们当作已知为真的事实用于计划生成。如果远端实际违反了这些假设,你可能会拿到错误的结果。

一个 FDW 到底实现了什么 ​

从内核视角看,一个 FDW 就是一个实现了若干回调函数(callback)的扩展。核心会调用这些回调来完成工作:

回调类别代表函数何时被调用
连接管理GetForeignRelSize、GetForeignPaths生成计划时,估算代价并产生访问路径
计划生成GetForeignPlan、BeginForeignScan生成扫描计划、准备执行
执行阶段IterateForeignScan、ReScanForeignScan逐行取数、重新扫描
写入阶段BeginForeignModify、ExecForeignInsert执行 INSERT / UPDATE / DELETE
远程 SQL 生成GetForeignJoinPaths、PlanForeignModify决定 JOIN、聚集等的下推
清理EndForeignScan、EndForeignModify释放资源、归还连接

PostgreSQL 还提供了一批辅助函数(helper functions),让 FDW 作者不必从零实现这些逻辑。这部分属于 FDW 开发者的领域,业务用户无需关心;如果你对"自己写一个 FDW"感兴趣,可以参看官方 Chapter 58。

注意:PostgreSQL 并未实现 SQL 标准里定义的那套 FDW API,它提供的是自己的一套接口。因此基于 PostgreSQL FDW 接口写的扩展无法直接移植到其他数据库。

权限模型:三个独立的授权力点 ​

使用 FDW 需要三处权限,缺一不可:

sql
-- 1. 使用 FDW 本身的权限
GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO app_user;

-- 2. 使用服务器(能连)的权限
GRANT USAGE ON FOREIGN SERVER remote_pg TO app_user;

-- 3. 查询外部表(能读这张表)的权限
GRANT SELECT ON remote_orders TO app_user;

-- 最后别忘了底层 schema 的权限
GRANT USAGE ON SCHEMA public TO app_user;

常见故障:表权限、server 权限都给了,但忘了 USAGE ON FOREIGN DATA WRAPPER,结果报 permission denied for foreign-data wrapper postgres_fdw。

何时该用 FDW ​

FDW 不是银弹,它有明确的适用面:

适合的场景

  • 跨库只读联合查询、报表、数据分析;
  • 需要把外部数据"纳入 SQL 统一视野",且两端数据量在可控范围内;
  • 归档/日志文件需要临时性 SQL 查询;
  • 分库分表中需要少量跨分片查询(低频、小结果集)。

不适合的场景

  • 高频写入远端(每次写入都是一次网络往返,代价远高于本地写);
  • 大表之间的大规模 JOIN(网络传输量可能远大于收益);
  • 需要强一致、低延迟的核心交易链路;
  • 数据量远超本地内存/磁盘能力,且无法用下推把结果集压小。

判断的核心原则:FDW 的价值在于"少搬数据"。如果一条 FDW 查询不得不把远端全表拉回本地再算,那它通常不如老老实实做 ETL。

FDW 与其他跨库方案的对比 ​

方案抽象层类型安全可下推事务适用
FDW表强(有列定义)是支持 2PCSQL 联合查询
dblink函数弱(返回 record)否支持 2PC一次性命令式访问
逻辑复制表(副本)强—支持需要本地副本
ETL表(副本)强——大批量、离线
应用层聚合无取决于代码否由应用决定灵活但复杂

dblink 适合"我就想执行一条远端 SQL,拿回一个值"这种即时性场景;FDW 适合"我要把远端数据当表长期使用"的场景。两者可以共存。

实战示例 ​

示例一:查询远程 PostgreSQL 的订单并统计 ​

假设远端有一张 orders 表,我们想在本地做近 7 天的租户维度统计。

sql
-- 环境准备(只需执行一次)
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

CREATE SERVER remote_pg
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.0.5', port '5432', dbname 'production');

CREATE USER MAPPING FOR CURRENT_USER
    SERVER remote_pg
    OPTIONS (user 'report_user', password 'report_pwd');

CREATE FOREIGN TABLE remote_orders (
    id          bigint,
    tenant_id   bigint,
    amount      numeric(12,2),
    status      text,
    created_at  timestamptz
)
SERVER remote_pg
OPTIONS (schema_name 'public', table_name 'orders');

执行统计查询:

sql
EXPLAIN (VERBOSE, COSTS OFF)
SELECT tenant_id, count(*) AS cnt, sum(amount) AS total
FROM remote_orders
WHERE created_at >= now() - interval '7 days'
  AND status = 'paid'
GROUP BY tenant_id
ORDER BY total DESC
LIMIT 20;

模拟输出如下(可以观察到聚合、过滤、排序、LIMIT 都被下推了):

text
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Limit
   Output: tenant_id, (count(*)), (sum(amount))
   ->  Sort
         Output: tenant_id, (count(*)), (sum(amount))
         Sort Key: (sum(remote_orders.amount)) DESC
         ->  Foreign Scan
               Output: tenant_id, (count(*)), (sum(amount))
               Relations: Aggregate on (public.remote_orders)
               Remote SQL: SELECT tenant_id, count(*), sum(amount) FROM public.orders
                           WHERE ((created_at >= (now() - '7 days'::interval)) AND (status = 'paid'))
                           GROUP BY tenant_id ORDER BY (sum(amount)) DESC NULLS LAST LIMIT 20
(9 rows)

关键信息是 Remote SQL 那一行:过滤、聚合、排序、LIMIT 全部被塞进了发给远端的 SQL,本地只负责接收 20 行结果。这是最理想的下推形态。

示例二:把日志文件当表查 ​

file_fdw 让我们可以不给数据库写一行 ETL 代码,就分析服务器上的日志文件:

sql
CREATE EXTENSION IF NOT EXISTS file_fdw;

CREATE SERVER log_server FOREIGN DATA WRAPPER file_fdw;

CREATE FOREIGN TABLE pg_csvlog (
    log_time        timestamp(3) with time zone,
    user_name       text,
    database_name   text,
    process_id      integer,
    connection_from text,
    session_id      text,
    session_line_num bigint,
    command_tag     text,
    error_severity  text,
    sql_state_code  text,
    message         text,
    detail          text,
    hint            text,
    query           text,
    application_name text
)
SERVER log_server
OPTIONS (filename '/var/lib/postgresql/18/data/log/postgresql.csv', format 'csv', header 'true');
sql
-- 统计最近日志中各条 SQL 状态码的出现次数
SELECT sql_state_code, count(*) AS n
FROM pg_csvlog
WHERE log_time > now() - interval '1 day'
  AND sql_state_code IS NOT NULL
GROUP BY sql_state_code
ORDER BY n DESC
LIMIT 10;
text
 sql_state_code |   n
----------------+-------
 00000          | 48213
 23505          |   142
 42P01          |    37
 40001          |    11
(4 rows)

sql_state_code 为空说明不是错误日志(00000 即 successful completion)。这类分析如果用 Shell + awk 写会非常痛苦,用 SQL 写就只是几行。

注意事项 ​

约束不被强制,只被"信任"

外部表上的 NOT NULL、CHECK、PRIMARY KEY 都只是给优化器的承诺,核心层不校验。远端数据可能违反它们:

sql
-- 可以这样声明
CREATE FOREIGN TABLE remote_orders (
    id bigint NOT NULL,
    amount numeric(12,2) CHECK (amount >= 0)
) SERVER remote_pg OPTIONS (schema_name 'public', table_name 'orders');

-- 但远端若真有 amount < 0 的行,这里不会报错,你会直接读到负数
SELECT * FROM remote_orders WHERE amount < 0;  -- 不推荐:说明约束声明与真实情况不符

列必须按名字对应,不按位置

外部表与远端表的列匹配是基于列名的(当不指定 column_name 选项时)。远端表改名或加列,都可能让本地查询悄悄出错。

sql
-- 错误!远端表实际列名是 order_amount,本地写成 amount 会取不到值
CREATE FOREIGN TABLE bad_orders (amount numeric)
SERVER remote_pg OPTIONS (schema_name 'public', table_name 'orders');

不要用外部表代替本地缓存

sql
-- 不推荐:每次调用都跨网络查一次
CREATE VIEW v_hot_config AS
SELECT * FROM remote_pg_config WHERE key = 'feature_flags';

对高频读取的小配置类数据,正确做法是用本地表 + 定时同步(或逻辑复制)。

ONLY 关键字对外部表无效

sql
-- 错误!外部表没有继承概念,ONLY 不起作用
SELECT * FROM ONLY remote_orders;

因为外部表不可能有子表,写 ONLY 没有意义(虽然在语法上是接受的)。

大结果集必须配合 LIMIT 或聚合

外部表查询若返回百万行,数据会真实地流过网络。养成习惯:

sql
-- 不推荐:可能拉回全表
SELECT * FROM remote_orders;

-- 推荐:先聚合再返回
SELECT count(*) FROM remote_orders;

跨库写操作的性能陷阱

sql
-- 不推荐:一次性 INSERT 一万行,可能产生一万次往返
INSERT INTO remote_orders SELECT * FROM local_staging;

大批量写入应尽量用 COPY(postgres_fdw 支持 COPY 到外部表),或分批提交。

密码明文存在系统目录中

USER MAPPING 里的 password 以明文形式存储在 pg_user_mapping 中,只有超级用户能读。生产环境更推荐使用 .pgpass 文件或 SSL 证书方案,避免密码出现在 DDL 语句与备份里。

ANALYZE 缺失会导致计划劣化

外部表若从未被分析过,优化器只能靠默认假设估算行数,容易选出糟糕的 JOIN 顺序:

bash
# 定期分析关键外部表
psql -c "ANALYZE remote_orders;"

统计信息在远端变化后需要重新收集

远端表数据分布变化后,本地缓存的外部表统计信息会过时。可以通过 ALTER FOREIGN TABLE ... OPTIONS (SET ...) 或定期 ANALYZE 缓解。

删除 server 会连带删除其下所有外部表

sql
-- 危险!会级联删除该 server 下所有外部表定义
DROP SERVER remote_pg CASCADE;  -- 不推荐:先确认清单

执行前先查询清单确认影响范围:

sql
SELECT c.relname
FROM pg_foreign_table ft
JOIN pg_class c ON c.oid = ft.ftrelid
JOIN pg_foreign_server s ON s.oid = ft.ftserver
WHERE s.srvname = 'remote_pg';

最佳实践 ​

命名带上语义前缀,一眼分辨内外

外部表名建议加前缀(如 remote_、ext_、fdw_),server 名带数据源标识(如 legacy_mysql、report_pg)。这样在读别人的 SQL 时,能立刻看出哪些操作会走网络。

sql
CREATE SERVER report_pg FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.0.5', dbname 'report');
CREATE FOREIGN TABLE remote_orders (...) SERVER report_pg OPTIONS (...);

优先用 IMPORT FOREIGN SCHEMA 保持列定义同步

手写列定义容易与远端漂移,导入方式能自动生成,且能通过 LIMIT TO / EXCEPT 精确控制范围:

sql
IMPORT FOREIGN SCHEMA public
    EXCEPT (internal_tmp, audit_log)
    FROM SERVER report_pg
    INTO reporting;

为只读场景创建专用账号

不要复用业务主账号。远端的映射用户应只有 SELECT 权限,从源头限制误写风险:

sql
CREATE USER MAPPING FOR CURRENT_USER SERVER report_pg
    OPTIONS (user 'fdw_readonly', password 'xxx');

集中管理连接信息

所有 server 定义建议写成可重复执行的迁移脚本(带 IF NOT EXISTS),不要手工在生产库敲命令:

sql
CREATE SERVER IF NOT EXISTS report_pg
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host '10.0.0.5', port '5432', dbname 'report');

用 IMPORT FOREIGN SCHEMA 之后立刻 ANALYZE

导入后立即收集统计信息,避免优化器长时间使用默认假设:

sql
ANALYZE remote_orders;

把下推能力作为选型硬指标

评估任何 FDW 时,第一件事是确认它支持哪些下推(WHERE / JOIN / 聚集 / ORDER BY / LIMIT)。不支持下推的 FDW,在大数据量下基本不可用。

用 EXPLAIN (VERBOSE) 的 Remote SQL 做验收

每次新增外部表或修改查询后,看一遍 Remote SQL,确认该下推的确实下推了:

sql
EXPLAIN (VERBOSE, COSTS OFF) SELECT ... FROM remote_orders WHERE ...;

为外部表单独建 schema

把外部表集中放到 foreign_ 或 fdw schema 下,既避免与本地表重名,也便于统一授权:

sql
CREATE SCHEMA foreign_data;
IMPORT FOREIGN SCHEMA public FROM SERVER report_pg INTO foreign_data;

不要滥用转发式视图

用视图把外部表包装成"假本地表"会掩盖网络开销,让后续维护者误判。若确需封装,视图名也要保留外部语义。

为跨库查询设置超时与资源上限

外部查询可能因为网络问题长时间挂住,建议为相关角色设置:

sql
ALTER ROLE report_user SET statement_timeout = '30s';

定期审计外部表的可达性

远端实例迁移或账号过期后,外部表会突然报错。建议加一个巡检任务,对所有 server 做一次轻量探测:

sql
-- 用一条极轻量的查询探测连通性
SELECT 1 FROM remote_orders LIMIT 1;

文档化每个外部表的来源

在表注释里写清远端地址与负责人,这比任何架构图都管用:

sql
COMMENT ON FOREIGN TABLE remote_orders IS
    '来源:report_pg(10.0.0.5) public.orders;负责人:数据组';

下一节

继续学习:postgres_fdw 外部数据包装器

参考链接 ​