Foreign Data Wrappers for external data access

Maria Garcia Feb 2026
2 tabs
-- Install postgres_fdw extension
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

-- Create foreign server
CREATE SERVER remote_db
  FOREIGN DATA WRAPPER postgres_fdw
  OPTIONS (
    host 'remote-host.example.com',
    port '5432',
    dbname 'remote_database'
  );

-- Create user mapping
CREATE USER MAPPING FOR current_user
  SERVER remote_db
  OPTIONS (
    user 'remote_user',
    password 'remote_password'
  );

-- Import foreign schema
IMPORT FOREIGN SCHEMA public
  LIMIT TO (users, orders)
  FROM SERVER remote_db
  INTO public;

-- Query foreign table
SELECT * FROM users WHERE created_at >= '2024-01-01';
-- Query executes on remote server

-- Create specific foreign table
CREATE FOREIGN TABLE remote_products (
  id INT,
  name VARCHAR(100),
  price DECIMAL(10,2),
  created_at TIMESTAMP
)
SERVER remote_db
OPTIONS (
  schema_name 'public',
  table_name 'products'
);

-- Join local and remote tables
SELECT
  l.username,
  COUNT(r.id) AS order_count
FROM local_users l
LEFT JOIN remote_orders r ON l.id = r.user_id
GROUP BY l.username;

-- File FDW (query CSV files)
CREATE EXTENSION file_fdw;

CREATE SERVER file_server
  FOREIGN DATA WRAPPER file_fdw;

CREATE FOREIGN TABLE csv_import (
  id INT,
  name TEXT,
  email TEXT,
  created_at TEXT
)
SERVER file_server
OPTIONS (
  filename '/data/users.csv',
  format 'csv',
  header 'true'
);

SELECT * FROM csv_import LIMIT 10;

-- Import CSV data to local table
INSERT INTO users (id, name, email, created_at)
SELECT
  id,
  name,
  email,
  created_at::TIMESTAMP
FROM csv_import;

-- Writable foreign tables
UPDATE remote_products
SET price = price * 1.1
WHERE category = 'electronics';

INSERT INTO remote_products (name, price)
VALUES ('New Product', 99.99);

-- Transaction limitations
BEGIN;
UPDATE local_table SET status = 'processed';
UPDATE remote_table SET status = 'processed';
COMMIT;
-- If remote fails, local changes still committed!

-- Better: Use 2-phase commit (if supported)
-- Or handle in application logic

-- MySQL FDW (requires mysql_fdw extension)
/*
CREATE EXTENSION mysql_fdw;

CREATE SERVER mysql_server
  FOREIGN DATA WRAPPER mysql_fdw
  OPTIONS (host 'mysql-host', port '3306');

CREATE USER MAPPING FOR current_user
  SERVER mysql_server
  OPTIONS (username 'mysql_user', password 'password');

CREATE FOREIGN TABLE mysql_products (
  id INT,
  name TEXT,
  price NUMERIC
)
SERVER mysql_server
OPTIONS (dbname 'mydb', table_name 'products');

SELECT * FROM mysql_products;
*/

-- Predicate pushdown (WHERE clause sent to remote)
EXPLAIN VERBOSE
SELECT * FROM remote_users
WHERE status = 'active'
  AND created_at >= '2024-01-01';
-- WHERE clause pushed to remote server

-- Without pushdown (fetches all rows)
SELECT * FROM remote_users
WHERE complex_function(status) = true;
-- complex_function() prevents pushdown
2 files · sql Explain with highlit

Foreign Data Wrappers (FDW) query external data sources as tables. I use postgresfdw for remote PostgreSQL databases. filefdw reads CSV files. mysql_fdw connects to MySQL. Understanding FDW limitations prevents surprises. Predicates push down to remote servers when possible. JOINs between foreign tables can be slow. Transactions don't span FDW boundaries. Proper FDW use federates data without ETL. Essential for data integration, legacy systems, microservices. PostgreSQL FDW enables SQL queries across heterogeneous data sources.