Build a disposable postgres_fdw link to a local partner database, inspect server/user-mapping catalogs and Remote SQL pushdown, inject a remote failure, and reason about transaction boundaries without pretending FDW is distributed two-phase commit.

Foreign Data Wrappers, Foreign Tables, Pushdown, Transactions, and Federated Queries

Build a disposable postgres_fdw link to a local partner database, inspect server/user-mapping catalogs and Remote SQL pushdown, inject a remote failure, and reason about transaction boundaries without pretending FDW is distributed two-phase commit.

Intermediate → Advanced180–240 minutesPostgreSQL extensibility and federationPostgreSQL 18.6 baselineCore + PostgreSQL supplied extensions only in mandatory labsServiceHub disposable objects: ch19_*Admin/superuser required where postgres_fdw or untrusted-code boundaries are demonstratedFree local tooling; no managed service requiredLast reviewed: August 2026

Learning outcomes

ServiceHub must read partner dispatch records that live in another PostgreSQL database. Copying that data on every request creates stale replicas; giving the application a second database connection pushes federation logic into every service. A Foreign Data Wrapper (FDW) lets PostgreSQL expose external data as foreign tables, but network, authentication, remote optimizer, and cross-system transaction limits remain real.

01

Install postgres_fdw and build a disposable partner database/server/user mapping.

02

Inspect foreign-server, user-mapping, and foreign-table metadata without exposing credentials.

03

Use EXPLAIN VERBOSE to see Remote SQL and identify predicate/join/aggregate pushdown.

04

Observe remote transaction snapshot behavior and controlled connection failure.

05

Explain why postgres_fdw is not distributed two-phase commit or automatic global business atomicity.

Topology

The lab uses one local PostgreSQL 18.6 instance on port 55432 with two databases: servicehub_lab (local) and servicehub_partner (remote from the FDW viewpoint). TCP/libpq still crosses a database connection boundary, so FDW planning, authentication, remote transactions, and failure handling remain observable without paid infrastructure.

1. Create the disposable partner database and least-privilege remote role

Run this administrative setup from the normal local cluster. PostgreSQL roles are cluster-wide, but table privileges remain database/object specific.

sql · admin: create remote login and partner database
DROP DATABASE IF EXISTS servicehub_partner WITH (FORCE);DO $$BEGIN  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ch19_partner_fdw') THEN    CREATE ROLE ch19_partner_fdw LOGIN;  END IF;END$$;CREATE DATABASE servicehub_partner;
psql · set a disposable password and build partner data
\password ch19_partner_fdw\connect servicehub_partnerCREATE SCHEMA partner;CREATE TABLE partner.dispatch (  dispatch_id bigint PRIMARY KEY,  region text NOT NULL,  status text NOT NULL,  scheduled_at timestamptz NOT NULL,  estimated_minutes integer NOT NULL CHECK (estimated_minutes >= 0));INSERT INTO partner.dispatchSELECT 190000 + g,       (ARRAY['north','south','east','west'])[(g % 4)+1],       (ARRAY['queued','assigned','closed'])[(g % 3)+1],       TIMESTAMPTZ '2026-08-18 06:00+00' + (g || ' minutes')::interval,       15 + (g % 120)FROM generate_series(1,5000) AS g;GRANT CONNECT ON DATABASE servicehub_partner TO ch19_partner_fdw;GRANT USAGE ON SCHEMA partner TO ch19_partner_fdw;GRANT SELECT, INSERT, UPDATE, DELETE ON partner.dispatch TO ch19_partner_fdw;ANALYZE partner.dispatch;\connect servicehub_lab

The password belongs only to this disposable lab. Do not put production credentials into lesson HTML, source control, application logs, or a shared SQL migration file.

2. Install postgres_fdw and define the remote server

postgres_fdw is a PostgreSQL-supplied extension. Unlike the trusted list used in Lesson 1, installing it normally requires an administrative/superuser path. After installation, ordinary roles can be granted controlled USAGE on a foreign server.

sql · local database: extension, server, and user mapping
CREATE EXTENSION IF NOT EXISTS postgres_fdw;DROP SERVER IF EXISTS ch19_partner_server CASCADE;CREATE SERVER ch19_partner_serverFOREIGN DATA WRAPPER postgres_fdwOPTIONS (  host '127.0.0.1',  port '55432',  dbname 'servicehub_partner');CREATE USER MAPPING FOR servicehub_ownerSERVER ch19_partner_serverOPTIONS (  user 'ch19_partner_fdw',  password '<LAB_PASSWORD>');GRANT USAGE ON FOREIGN SERVER ch19_partner_server TO servicehub_owner;

Substitute the password interactively when running the lab; do not preserve a real secret in the file. PostgreSQL restricts visibility of sensitive user-mapping options. Use pg_user_mappings for governance rather than querying the underlying catalog as an ordinary user.

3. Define a foreign table with an explicit schema contract

sql · local: foreign table definition
DROP FOREIGN TABLE IF EXISTS app.ch19_partner_dispatch;CREATE FOREIGN TABLE app.ch19_partner_dispatch (  dispatch_id bigint NOT NULL,  region text NOT NULL,  status text NOT NULL,  scheduled_at timestamptz NOT NULL,  estimated_minutes integer NOT NULL)SERVER ch19_partner_serverOPTIONS (  schema_name 'partner',  table_name 'dispatch');ALTER FOREIGN TABLE app.ch19_partner_dispatch OWNER TO servicehub_owner;

A foreign table stores no partner rows locally. Its column definition is a local contract used for parsing, planning, and conversion. postgres_fdw does not automatically validate every local/remote type mismatch at CREATE time, so schema drift can surface later at query execution.

4. Observe FDW metadata safely

sql · server, mapping, and foreign-table catalogs
SELECT s.srvname,       f.fdwname,       pg_get_userbyid(s.srvowner) AS server_owner,       s.srvoptionsFROM pg_foreign_server AS sJOIN pg_foreign_data_wrapper AS f ON f.oid = s.srvfdwWHERE s.srvname = 'ch19_partner_server';SELECT srvname, usename, umoptionsFROM pg_user_mappingsWHERE srvname = 'ch19_partner_server';SELECT c.oid::regclass AS foreign_table,       s.srvname,       ft.ftoptionsFROM pg_foreign_table AS ftJOIN pg_class AS c ON c.oid = ft.ftrelidJOIN pg_foreign_server AS s ON s.oid = ft.ftserverWHERE c.oid = 'app.ch19_partner_dispatch'::regclass;

The mapping view masks options when the current role lacks the rights to see them. Treat database catalog access as part of secret governance; do not build monitoring that indiscriminately exports user-mapping option arrays.

5. EXPLAIN VERBOSE shows what is actually sent remotely

sql · predicate/projection pushdown
EXPLAIN (VERBOSE, COSTS OFF)SELECT dispatch_id, scheduled_atFROM app.ch19_partner_dispatchWHERE region = 'north'  AND status = 'assigned'ORDER BY scheduled_atLIMIT 20;

Inspect the Remote SQL line. postgres_fdw attempts to ship safe immutable built-in conditions, projections, ordering, limits, joins, and aggregates when it can prove compatible semantics. The goal is to reduce rows/columns crossing the network.

sql · aggregate pushdown evidence
EXPLAIN (VERBOSE, COSTS OFF)SELECT region, count(*), avg(estimated_minutes)FROM app.ch19_partner_dispatchWHERE status <> 'closed'GROUP BY regionORDER BY region;

If the aggregate is pushed down, the remote SQL contains grouping/aggregation instead of returning all qualifying base rows. If a local expression/function cannot be safely shipped, PostgreSQL can fetch rows and evaluate that piece locally.

6. Why some expressions remain local

sql · local volatile expression prevents full remote execution
EXPLAIN (VERBOSE, COSTS OFF)SELECT dispatch_id,       scheduled_at,       clock_timestamp() AS observed_locallyFROM app.ch19_partner_dispatchWHERE region = 'east'LIMIT 10;

Remote pushdown is correctness-sensitive. postgres_fdw avoids shipping arbitrary user functions/operators unless they are built-in or explicitly declared in the server's extensions option and meet immutability requirements. It cannot assume a function has identical definition/semantics on the other server merely because the name exists.

7. Remote transaction snapshots can surprise READ COMMITTED users

When a local transaction first touches a foreign server, postgres_fdw opens a corresponding remote transaction. Unless the local transaction is SERIALIZABLE, the remote transaction uses REPEATABLE READ, giving snapshot-consistent repeated scans on that remote server.

sql · session A: local transaction sees a stable remote snapshot
BEGIN;SELECT statusFROM app.ch19_partner_dispatchWHERE dispatch_id = 190001;-- Keep this transaction open.-- In another direct connection to servicehub_partner, update 190001 and COMMIT.SELECT statusFROM app.ch19_partner_dispatchWHERE dispatch_id = 190001;COMMIT;

The second foreign scan in the same local transaction can still see the earlier remote snapshot even if the direct partner session committed a change. This is deliberate consistency behavior, not stale connection caching.

8. Controlled remote failure

sql · temporarily point the server at an unused port
BEGIN;ALTER SERVER ch19_partner_serverOPTIONS (SET port '59999');SELECT count(*) FROM app.ch19_partner_dispatch;-- Expected: connection failure from postgres_fdw/libpq.ROLLBACK;SELECT count(*) FROM app.ch19_partner_dispatch;

The foreign query fails; PostgreSQL does not silently return an empty relation. The rollback restores server metadata. Applications using federation need timeouts, retry classification, circuit-breaking/partial-service decisions, and observability for remote connection wait events.

9. The cross-system transaction boundary

postgres_fdw mirrors local transactions and savepoints with remote transactions/subtransactions. But it does not support preparing the remote transaction for two-phase commit. With multiple remote systems, commit work can occur across them without a global prepared transaction that makes all final commits atomic under failures.

Wrong assumption

BEGIN; UPDATE local_table; UPDATE foreign_table; COMMIT; does not turn separate PostgreSQL systems into one distributed consensus transaction. Ordinary success is coordinated, but failure during final commits can require reconciliation. For money/entitlement workflows, design idempotency, outbox/saga/reconciliation or a deliberately engineered distributed-transaction solution rather than assuming FDW provides one.

10. Verification and cleanup

sql · foreign access acceptance and later teardown
SET ROLE servicehub_owner;SELECT count(*) AS partner_rowsFROM app.ch19_partner_dispatch;RESET ROLE;-- At the END of Chapter 19:-- DROP FOREIGN TABLE app.ch19_partner_dispatch;-- DROP USER MAPPING FOR servicehub_owner SERVER ch19_partner_server;-- DROP SERVER ch19_partner_server;-- DROP EXTENSION postgres_fdw;

Check your understanding

  1. What is the difference between a foreign server and a foreign table?
  2. Where do you inspect the actual pushed-down SQL?
  3. Why can one local READ COMMITTED transaction see a stable remote snapshot?
  4. What happens when the remote server becomes unreachable?
  5. Why is postgres_fdw not automatic distributed two-phase commit?
Review the answers

A server describes a remote endpoint/FDW; a foreign table maps a local relation definition to remote data. EXPLAIN VERBOSE exposes Remote SQL. postgres_fdw uses a stable remote transaction snapshot (REPEATABLE READ unless the local transaction is SERIALIZABLE). Remote failures raise errors. postgres_fdw does not prepare remote transactions for 2PC, so global commit atomicity across multiple systems is not guaranteed by FDW alone.

Authoritative references

Extension, FDW, language, and server-code behavior is privilege-, package-, version-, and platform-sensitive. These PostgreSQL 18 primary sources define the mechanisms used here.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.