Foreign Data Wrapper(FDW):跨資料庫查詢的瑞士刀 | PostgreSQL
PostgreSQL 的 Foreign Data Wrapper(FDW) 實作了 SQL/MED 標準,讓你透過標準 SQL 語法查詢遠端 PostgreSQL、MySQL、Oracle、CSV 檔案甚至 REST API——如同操作本地表格一般。本文從 FDW 三層架構到 Pushdown 優化原理,完整解析這個跨資料庫整合的利器。
什麼是 FDW?
Foreign Data Wrapper(外部資料封裝器)是 PostgreSQL 實作 SQL/MED(Management of External Data,ISO/IEC 9075-9)標準的機制。它將外部資料源的連線、協議轉換、查詢優化等複雜性封裝在擴展層,讓應用程式只需操作 Foreign Table 即可存取各種異質資料源。
PostgreSQL 自 9.1 版起提供完整的 SQL/MED 實作,使其能夠作為資料整合中心(Data Integration Hub),統一存取多個外部資料源。
FDW 三層架構
FDW 由三個相互依存的資料庫物件組成,形成完整的連線與存取鏈:
┌─────────────────────────────────────────────────────┐
│ 客戶端查詢 │
│ SELECT * FROM orders o JOIN remote.users u ... │
└────────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Foreign Table(外部表格) │
│ 定義欄位結構,指向 Foreign Server 上的遠端物件 │
└────────────────────────┬────────────────────────────┘
│ 使用哪個伺服器?
▼
┌─────────────────────────────────────────────────────┐
│ Foreign Server(外部伺服器) │
│ 描述遠端資料源的位址、連接埠、資料庫名稱 │
└────────────────────────┬────────────────────────────┘
│ 用哪個身份連線?
▼
┌─────────────────────────────────────────────────────┐
│ User Mapping(使用者映射) │
│ 將本地使用者身份映射到遠端的認證憑證 │
└─────────────────────────────────────────────────────┘
- Foreign Data Wrapper:定義存取特定類型資料源的驅動程式(如
postgres_fdw、file_fdw) - Foreign Server:代表一個外部資料源實例,儲存主機、連接埠等連線資訊
- User Mapping:將本地使用者映射至遠端的認證憑證(帳號、密碼)
- Foreign Table:本地可見的虛擬表格,定義欄位結構並指向 Foreign Server
Pushdown 優化機制
Pushdown(下推) 是 FDW 最重要的效能優化機制。核心思想是將 SQL 操作(WHERE 過濾、JOIN、排序、彙總)推送至遠端執行,減少網路傳輸的資料量。
沒有 Pushdown:把整張遠端表格拉回來,本地再過濾 → 傳輸 100 萬筆,只用 100 筆
有 Pushdown:遠端先過濾,只傳回結果 → 只傳輸 100 筆,效能大幅提升
postgres_fdw 支援的 Pushdown 類型(截至 PostgreSQL 16):
| Pushdown 類型 | 說明 | 支援版本 |
|---|---|---|
| WHERE Pushdown | 過濾條件在遠端執行 | 9.1+ |
| JOIN Pushdown | 兩張 Foreign Table 的 JOIN 在遠端執行 | 14+ |
| Sort Pushdown | ORDER BY 在遠端執行 | 14+ |
| Aggregate Pushdown | COUNT/SUM/AVG 等彙總在遠端執行 | 11+ |
| LIMIT/OFFSET Pushdown | 分頁限制在遠端執行 | 14+ |
postgres_fdw 完整設定流程
postgres_fdw 是 PostgreSQL 官方提供的 FDW,專門用於存取遠端 PostgreSQL 資料庫。
步驟一:安裝擴展
-- 安裝 postgres_fdw 擴展
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- 確認安裝成功
SELECT * FROM pg_extension WHERE extname = 'postgres_fdw';
步驟二:建立 Foreign Server
-- 建立指向遠端 PostgreSQL 實例的 Foreign Server
CREATE SERVER remote_prod
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (
host '10.0.1.100', -- 遠端主機位址
port '5432', -- 遠端連接埠
dbname 'production', -- 遠端資料庫名稱
connect_timeout '10', -- 連線逾時(秒)
keepalives '1', -- 啟用 TCP keepalive
keepalives_idle '60' -- keepalive 閒置時間
);
-- 查看已建立的 Foreign Server
SELECT * FROM pg_foreign_server;
步驟三:建立 User Mapping
-- 為特定本地使用者建立遠端認證映射
CREATE USER MAPPING FOR app_user
SERVER remote_prod
OPTIONS (
user 'remote_reader',
password 'secure_password_here'
);
-- 為 DBA 建立管理員映射
CREATE USER MAPPING FOR postgres
SERVER remote_prod
OPTIONS (
user 'remote_admin',
password 'admin_password'
);
步驟四:建立 Foreign Table
方法 A:手動建立
-- 手動定義 Foreign Table(需與遠端表格結構對應)
CREATE FOREIGN TABLE remote_users (
id BIGINT NOT NULL,
username VARCHAR(100) NOT NULL,
email VARCHAR(255),
created_at TIMESTAMP NOT NULL,
status SMALLINT DEFAULT 1
)
SERVER remote_prod
OPTIONS (
schema_name 'public', -- 遠端 schema 名稱
table_name 'users' -- 遠端表格名稱
);
方法 B:IMPORT FOREIGN SCHEMA 批次匯入
-- 建立本地 schema 接收匯入的表格
CREATE SCHEMA IF NOT EXISTS remote_schema;
-- 一次匯入遠端整個 schema 下的所有表格
IMPORT FOREIGN SCHEMA public
FROM SERVER remote_prod
INTO remote_schema;
-- 只匯入指定表格
IMPORT FOREIGN SCHEMA public
LIMIT TO (users, orders, products)
FROM SERVER remote_prod
INTO remote_schema;
-- 排除特定表格
IMPORT FOREIGN SCHEMA public
EXCEPT (audit_logs, temp_tables)
FROM SERVER remote_prod
INTO remote_schema;
-- 確認匯入結果
SELECT foreign_table_schema, foreign_table_name
FROM information_schema.foreign_tables
ORDER BY 1, 2;
IMPORT FOREIGN SCHEMA 是生產環境的首選方法——自動對應欄位類型,省去手動維護的成本。
file_fdw:讀取 CSV 與日誌檔
file_fdw 是官方提供的 FDW,用於將本地檔案系統上的 CSV 或分隔符號格式文字檔映射為 Foreign Table:
-- 安裝 file_fdw 擴展
CREATE EXTENSION IF NOT EXISTS file_fdw;
-- 建立 Server(每個 file_fdw 通常只需一個)
CREATE SERVER local_files
FOREIGN DATA WRAPPER file_fdw;
-- 建立 Foreign Table 對應 CSV 檔案
CREATE FOREIGN TABLE import_orders (
order_id INTEGER,
customer TEXT,
amount NUMERIC(10, 2),
order_date DATE,
status TEXT
)
SERVER local_files
OPTIONS (
filename '/var/lib/postgresql/imports/orders.csv',
format 'csv',
header 'true', -- 第一列為標頭
delimiter ',', -- 欄位分隔符號
null 'NULL', -- NULL 值的表示方式
encoding 'UTF8'
);
-- 查詢 CSV 資料(如同本地表格)
SELECT * FROM import_orders WHERE amount > 1000;
讀取 PostgreSQL CSV 日誌
-- 將 PostgreSQL CSV 日誌映射為可查詢的表格
CREATE FOREIGN TABLE pg_log_import (
log_time TIMESTAMP 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,
session_start_time TIMESTAMP WITH TIME ZONE,
virtual_transaction_id TEXT,
transaction_id BIGINT,
error_severity TEXT,
sql_state_code TEXT,
message TEXT,
detail TEXT,
hint TEXT,
internal_query TEXT,
internal_query_pos INTEGER,
context TEXT,
query TEXT,
query_pos INTEGER,
location TEXT,
application_name TEXT
)
SERVER local_files
OPTIONS (
filename '/var/log/postgresql/postgresql-2026-07-13.csv',
format 'csv'
);
-- 分析日誌中的錯誤
SELECT log_time, error_severity, sql_state_code, message
FROM pg_log_import
WHERE error_severity IN ('ERROR', 'FATAL', 'PANIC')
ORDER BY log_time DESC
LIMIT 20;
注意:file_fdw 只支援讀取操作(唯讀),且檔案必須對 PostgreSQL 進程可讀。
跨資料庫 JOIN 實戰
以下示範實際業務場景:本地資料庫存放訂單,遠端資料庫存放客戶資料:
-- JOIN 本地訂單與遠端客戶資料
SELECT
o.order_id,
c.name AS customer_name,
c.email,
o.total,
o.status,
o.created_at
FROM orders o
JOIN remote_schema.customers c ON c.id = o.customer_id
WHERE o.status = 'pending'
AND o.created_at >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY o.total DESC
LIMIT 100;
-- 彙總查詢:各國客戶的總消費金額
SELECT
c.country,
COUNT(o.order_id) AS order_count,
SUM(o.total) AS total_revenue
FROM orders o
JOIN remote_schema.customers c ON c.id = o.customer_id
WHERE c.status = 'active'
GROUP BY c.country
ORDER BY total_revenue DESC;
用 EXPLAIN 驗證 Pushdown
使用 EXPLAIN (VERBOSE, ANALYZE) 可以查看實際發送至遠端的 SQL 語句:
-- 查看查詢計畫,確認 Pushdown 是否生效
EXPLAIN (VERBOSE, ANALYZE)
SELECT c.name, SUM(o.total)
FROM orders o
JOIN remote_schema.customers c ON c.id = o.customer_id
WHERE c.country = 'TW'
AND o.created_at >= '2026-01-01'
GROUP BY c.name;
典型輸出(Pushdown 生效時):
HashAggregate (cost=... rows=... width=...)
-> Hash Join (cost=... rows=... width=...)
Hash Cond: (o.customer_id = c.id)
-> Seq Scan on orders o (cost=...)
Filter: (created_at >= '2026-01-01')
-> Hash (cost=... rows=... width=...)
-> Foreign Scan on customers c (cost=...)
Output: c.name, c.id
Remote SQL: SELECT id, name FROM public.customers
WHERE ((country = 'TW'))
Remote SQL 欄位顯示實際傳送至遠端的 SQL——確認 WHERE 條件已被成功下推。
FDW 結合 Table Partitioning:分散式架構
PostgreSQL 的宣告式分割可以與 FDW 結合,建立橫跨多台實例的分散式查詢架構:
-- 建立分割主表(本地)
CREATE TABLE sales (
sale_id BIGINT NOT NULL,
region TEXT NOT NULL,
amount NUMERIC,
sale_date DATE NOT NULL
) PARTITION BY LIST (region);
-- 建立各遠端伺服器
CREATE SERVER shard_apac FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '10.0.1.10', port '5432', dbname 'sales_db');
CREATE SERVER shard_emea FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '10.0.1.20', port '5432', dbname 'sales_db');
CREATE SERVER shard_amer FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '10.0.1.30', port '5432', dbname 'sales_db');
-- 建立 User Mapping(略:同前述步驟三)
-- 建立 Foreign Table 作為分割分片
CREATE FOREIGN TABLE sales_apac
PARTITION OF sales FOR VALUES IN ('TW', 'JP', 'SG', 'AU', 'KR')
SERVER shard_apac
OPTIONS (schema_name 'public', table_name 'sales');
CREATE FOREIGN TABLE sales_emea
PARTITION OF sales FOR VALUES IN ('DE', 'FR', 'GB', 'NL', 'IT')
SERVER shard_emea
OPTIONS (schema_name 'public', table_name 'sales');
CREATE FOREIGN TABLE sales_amer
PARTITION OF sales FOR VALUES IN ('US', 'CA', 'MX', 'BR')
SERVER shard_amer
OPTIONS (schema_name 'public', table_name 'sales');
查詢時,PostgreSQL 自動根據分割鍵路由至對應分片:
-- Partition Pruning + FDW Pushdown 雙重優化
-- 只查詢 shard_apac,不會存取其他分片
SELECT region, SUM(amount) AS total
FROM sales
WHERE region = 'TW'
AND sale_date >= '2026-01-01'
GROUP BY region;
-- 跨分片彙總:自動查詢所有分片並合併結果
SELECT region, SUM(amount)
FROM sales
WHERE sale_date BETWEEN '2026-01-01' AND '2026-03-31'
GROUP BY region
ORDER BY 2 DESC;
這種架構實現了類似 Citus 的水平擴展,而 Partition Pruning 確保查詢只存取相關分片。
效能調校
fetch_size 調整
-- fetch_size 控制每次批次取回的列數(預設 100,對大型結果集過低)
-- Server 層級設定
ALTER SERVER remote_prod OPTIONS (SET fetch_size '5000');
-- Foreign Table 層級設定(覆蓋 Server 設定)
ALTER FOREIGN TABLE remote_schema.large_table
OPTIONS (ADD fetch_size '10000');
-- batch_size:INSERT 批次大小(PostgreSQL 14 新增)
ALTER SERVER remote_prod OPTIONS (ADD batch_size '100');
非同步執行(PostgreSQL 14+)
-- async_capable 允許非同步並行掃描多個 Foreign Table
ALTER SERVER remote_prod OPTIONS (ADD async_capable 'true');
ALTER SERVER remote_backup OPTIONS (ADD async_capable 'true');
-- 設定並行工作者數量
SET max_parallel_workers_per_gather = 4;
use_remote_estimate
-- 讓規劃器使用遠端 EXPLAIN 取得更準確的成本估算
-- 代價:每次規劃都會對遠端發出 EXPLAIN 查詢
ALTER SERVER remote_prod OPTIONS (ADD use_remote_estimate 'true');
-- 或手動調整統計樣本大小
ALTER FOREIGN TABLE remote_schema.customers
ALTER COLUMN id SET STATISTICS 1000;
ANALYZE remote_schema.customers;
連線管理
-- keep_connections 減少重複建立連線的成本(預設開啟)
ALTER SERVER remote_prod OPTIONS (SET keep_connections 'on');
-- 查看目前的 FDW 連線狀態
SELECT * FROM postgres_fdw_get_connections();
-- 手動清除閒置的 FDW 連線
SELECT postgres_fdw_disconnect('remote_prod');
SELECT postgres_fdw_disconnect_all();
常見陷阱與注意事項
| 陷阱 | 說明 | 建議 |
|---|---|---|
| 交易一致性 | FDW 不完全支援分散式 2PC | 盡量只做讀取,寫入改用本地 queue |
| 統計資訊不準 | Foreign Table 預設統計不準確 | 啟用 use_remote_estimate 或定期 ANALYZE |
| Pushdown 失效 | 使用本地函數或型別轉換會阻止 Pushdown | 用 EXPLAIN 確認 Remote SQL |
| fetch_size 過低 | 預設 100,大量資料查詢效能差 | 調高至 1000-10000 |
| 密碼明文存放 | User Mapping 密碼存在 pg_user_mappings | 限制 pg_user_mappings 存取權限 |
| SAVEPOINT 相容性 | ROLLBACK TO SAVEPOINT 會關閉遠端游標 | 避免在 FDW 查詢中使用 SAVEPOINT |
| 時區不一致 | 本地與遠端時區設定不同 | 統一使用 UTC |
| 網路延遲 | 高延遲網路會拖慢所有查詢 | 優先使用 Pushdown,減少往返次數 |
其他常用 FDW
oracle_fdw
-- 存取 Oracle 資料庫
CREATE EXTENSION oracle_fdw;
CREATE SERVER oracle_db
FOREIGN DATA WRAPPER oracle_fdw
OPTIONS (dbserver '//10.0.1.50:1521/ORCL');
CREATE USER MAPPING FOR app_user
SERVER oracle_db
OPTIONS (user 'oracle_user', password 'oracle_pass');
CREATE FOREIGN TABLE oracle_customers (
cust_id NUMERIC(10),
cust_name VARCHAR(100),
region VARCHAR(50)
)
SERVER oracle_db
OPTIONS (schema 'HR', table 'CUSTOMERS');
mysql_fdw
-- 存取 MySQL / MariaDB
CREATE EXTENSION mysql_fdw;
CREATE SERVER mysql_db
FOREIGN DATA WRAPPER mysql_fdw
OPTIONS (host '10.0.1.60', port '3306');
CREATE USER MAPPING FOR postgres
SERVER mysql_db
OPTIONS (username 'mysql_user', password 'mysql_pass');
CREATE FOREIGN TABLE mysql_products (
id INT NOT NULL,
name VARCHAR(255),
price NUMERIC(10, 2),
stock INT
)
SERVER mysql_db
OPTIONS (dbname 'shop', table_name 'products');
FDW Pushdown 支援矩陣
| FDW | WHERE | JOIN | Sort | Aggregate | LIMIT | 寫入 |
|---|---|---|---|---|---|---|
| postgres_fdw | 完整 | 完整(14+) | 完整(14+) | 完整(11+) | 完整(14+) | 支援 |
| file_fdw | 本地過濾 | 不支援 | 不支援 | 不支援 | 不支援 | 不支援 |
| oracle_fdw | 完整 | 部分 | 部分 | 部分 | 支援 | 支援 |
| mysql_fdw | 完整 | 部分 | 部分 | 部分 | 支援 | 支援 |
| tds_fdw | 部分 | 不支援 | 不支援 | 不支援 | 支援 | 支援 |
即使 FDW 宣告支援某類 Pushdown,規劃器仍可能基於成本估算選擇不使用——務必透過 EXPLAIN 確認。
FDW 版本演進
| PostgreSQL 版本 | FDW 重要功能 |
|---|---|
| 9.1 | 基本 FDW API,可讀取外部資料 |
| 9.3 | 可寫入 Foreign Table(INSERT/UPDATE/DELETE) |
| 9.6 | Upper-Relation Pushdown API |
| 10 | 宣告式分割 + FDW 可作為分片 |
| 11 | postgres_fdw Aggregate Pushdown |
| 12 | Partition-wise JOIN/Aggregate Pushdown |
| 14 | async_capable 非同步掃描、Sort/LIMIT Pushdown、batch_size |
| 15 | RLS 與 FDW 整合改善 |
| 16 | postgres_fdw 效能改善,更精準 Cost 估算 |
| 17 | Foreign Table 統計資訊收集效能優化 |
總結
Foreign Data Wrapper 是 PostgreSQL 作為資料整合中心的核心機制。掌握以下重點:
- 三層架構:Foreign Data Wrapper → Foreign Server → User Mapping → Foreign Table
- Pushdown 是效能關鍵:用
EXPLAIN (VERBOSE)確認Remote SQL確實包含過濾條件 IMPORT FOREIGN SCHEMA比手動建立 Foreign Table 更省事、更不容易出錯- fetch_size 預設 100 太低,生產環境建議調至 1000-10000
- FDW + Partitioning 可實現類似 Citus 的分散式查詢架構
- 交易一致性限制:FDW 不支援完整的 2PC,跨庫寫入需謹慎
下一篇,我們將探討 PostgreSQL 規則系統(Rule System)——了解 PostgreSQL 獨有的查詢重寫機制,以及它與 Trigger 的差異和適用場景。