Skip to content

Postgres FDW Best Practices

Objective: FDWs make Postgres the query engine for your data lake — but misuse them, and you'll summon performance demons. Here's how to tame them.

FDWs make Postgres the query engine for your data lake — but misuse them, and you'll summon performance demons. Here's how to tame them.

0) Prerequisites (Read Once, Live by Them)

The Five Commandments

  1. FDWs ≠ free lunch
  2. They delegate work to external engines/filesystems
  3. Network latency and I/O costs are real
  4. Plan for performance implications
  5. Monitor resource usage

  6. Schema drift is real

  7. Always validate column types before FDW creation
  8. Handle nullability mismatches gracefully
  9. Plan for schema evolution
  10. Document schema changes

  11. Push filtering/projection down whenever possible

  12. Reduce data transfer over network
  13. Leverage external engine optimizations
  14. Minimize Postgres processing overhead
  15. Use WHERE clauses aggressively

  16. Keep FDW schemas small, compose with materialized views

  17. Avoid massive monolithic FDW tables
  18. Use materialized views for heavy joins
  19. Cache frequently accessed data
  20. Plan for query optimization

  21. Security and governance are non-negotiable

  22. Store credentials securely
  23. Audit access patterns
  24. Limit FDW privileges
  25. Monitor for data leakage

Why These Principles: FDWs require understanding external data sources, performance implications, and security considerations. Understanding these patterns prevents performance chaos and enables reliable federated data access.

1) Core Principles

The FDW Reality

# What you thought FDWs were
fdw_fantasy:
  "performance": "FDWs are as fast as local tables"
  "simplicity": "Just point at files and query"
  "reliability": "FDWs never fail or change"
  "security": "FDWs are secure by default"

# What FDWs actually are
fdw_reality:
  "performance": "Network and I/O costs are real"
  "simplicity": "Complex configuration and maintenance"
  "reliability": "External dependencies can break"
  "security": "Credentials and access must be managed"

Why Reality Checks Matter: Understanding the true nature of FDWs enables proper planning and risk management. Understanding these patterns prevents performance chaos and enables reliable federated data access.

FDW Performance Characteristics

## FDW Performance Profile

### Network I/O
- **Latency**: 10-100ms per query (depending on distance)
- **Bandwidth**: Limited by network capacity
- **Caching**: Minimal, mostly at filesystem level
- **Concurrency**: Limited by external system capacity

### Memory Usage
- **Buffer**: Limited by external system
- **Sorting**: Often done in external system
- **Joins**: May require data transfer to Postgres
- **Aggregations**: Pushdown when possible

### CPU Usage
- **Pushdown**: Filters and projections
- **Local Processing**: Complex joins and aggregations
- **Type Conversion**: Overhead for data type mismatches
- **Query Planning**: Additional complexity

Why Performance Understanding Matters: Proper FDW performance planning enables efficient federated queries and prevents system overload. Understanding these patterns prevents performance chaos and enables reliable federated data access.

2) Common FDWs in Practice

parquet_fdw (Local Parquet Files)

-- Install parquet_fdw extension
CREATE EXTENSION IF NOT EXISTS parquet_fdw;

-- Create foreign server
CREATE SERVER parquet_server
FOREIGN DATA WRAPPER parquet_fdw
OPTIONS (
    filename '/path/to/data/'
);

-- Create foreign table
CREATE FOREIGN TABLE telemetry_parquet (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    location GEOMETRY(POINT, 4326)
) SERVER parquet_server
OPTIONS (
    filename '/path/to/data/telemetry_2024_01.parquet',
    format 'parquet'
);

-- Query with pushdown filters
SELECT sensor_id, temperature, timestamp
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
  AND temperature > 25.0
  AND sensor_id = 'sensor_001';

Why parquet_fdw Matters: Local parquet_fdw enables efficient querying of parquet files with pushdown optimization. Understanding these patterns prevents I/O chaos and enables reliable local data access.

parquet_s3_fdw (S3/MinIO/VAST)

-- Install parquet_s3_fdw extension
CREATE EXTENSION IF NOT EXISTS parquet_s3_fdw;

-- Create foreign server with credentials
CREATE SERVER parquet_s3_server
FOREIGN DATA WRAPPER parquet_s3_fdw
OPTIONS (
    aws_region 'us-west-2',
    aws_access_key_id 'your_access_key',
    aws_secret_access_key 'your_secret_key',
    aws_session_token 'your_session_token'  -- Optional
);

-- Create foreign table
CREATE FOREIGN TABLE telemetry_s3 (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    location GEOMETRY(POINT, 4326)
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/telemetry/2024/01/',
    format 'parquet'
);

-- Query with S3 pushdown
SELECT sensor_id, AVG(temperature) as avg_temp
FROM telemetry_s3
WHERE timestamp >= '2024-01-01'
  AND timestamp < '2024-02-01'
  AND sensor_id IN ('sensor_001', 'sensor_002')
GROUP BY sensor_id;

Why parquet_s3_fdw Matters: S3-based parquet_fdw enables cloud-scale data lake querying with pushdown optimization. Understanding these patterns prevents cloud chaos and enables reliable cloud data access.

duckdb_fdw (DuckDB Vectorized Execution)

-- Install duckdb_fdw extension
CREATE EXTENSION IF NOT EXISTS duckdb_fdw;

-- Create foreign server
CREATE SERVER duckdb_server
FOREIGN DATA WRAPPER duckdb_fdw
OPTIONS (
    database '/path/to/data.duckdb'
);

-- Create foreign table
CREATE FOREIGN TABLE telemetry_duckdb (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    location GEOMETRY(POINT, 4326)
) SERVER duckdb_server
OPTIONS (
    table 'telemetry'
);

-- Leverage DuckDB's vectorized execution
SELECT 
    sensor_id,
    DATE_TRUNC('hour', timestamp) as hour,
    AVG(temperature) as avg_temp,
    STDDEV(temperature) as temp_stddev
FROM telemetry_duckdb
WHERE timestamp >= '2024-01-01'
  AND temperature IS NOT NULL
GROUP BY sensor_id, DATE_TRUNC('hour', timestamp)
ORDER BY sensor_id, hour;

Why duckdb_fdw Matters: DuckDB FDW leverages vectorized execution for analytical workloads with excellent performance. Understanding these patterns prevents analytical chaos and enables reliable analytical data access.

postgres_fdw (Postgres-to-Postgres)

-- Install postgres_fdw extension
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

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

-- Create user mapping
CREATE USER MAPPING FOR current_user
SERVER remote_postgres
OPTIONS (
    user 'analytics_user',
    password 'secure_password'
);

-- Create foreign table
CREATE FOREIGN TABLE remote_analytics (
    id INTEGER,
    metric_name VARCHAR(100),
    metric_value DOUBLE PRECISION,
    timestamp TIMESTAMP WITH TIME ZONE,
    tags JSONB
) SERVER remote_postgres
OPTIONS (
    schema_name 'public',
    table_name 'analytics_metrics'
);

-- Query with pushdown
SELECT metric_name, AVG(metric_value) as avg_value
FROM remote_analytics
WHERE timestamp >= '2024-01-01'
  AND metric_name IN ('cpu_usage', 'memory_usage')
GROUP BY metric_name;

Why postgres_fdw Matters: Postgres-to-Postgres FDW enables federated queries across Postgres clusters with pushdown optimization. Understanding these patterns prevents federation chaos and enables reliable distributed data access.

3) Schema Management & Type Safety

Schema Discovery

-- Discover parquet schema using DuckDB
SELECT 
    column_name,
    data_type,
    is_nullable,
    comment
FROM parquet_metadata('/path/to/data.parquet')
ORDER BY column_name;

-- Alternative: Use parquet_schema function
SELECT parquet_schema('/path/to/data.parquet');

-- Check for schema drift
SELECT 
    column_name,
    data_type,
    is_nullable
FROM parquet_metadata('/path/to/data.parquet')
WHERE column_name IN ('temperature', 'humidity', 'pressure')
ORDER BY column_name;

Why Schema Discovery Matters: Proper schema discovery prevents type mismatches and query failures. Understanding these patterns prevents schema chaos and enables reliable data access.

Type Safety and Validation

-- Create wrapper function for type-safe FDW creation
CREATE OR REPLACE FUNCTION create_telemetry_fdw(
    table_name TEXT,
    file_path TEXT,
    server_name TEXT DEFAULT 'parquet_server'
) RETURNS VOID AS $$
DECLARE
    schema_info RECORD;
    column_def TEXT;
    column_defs TEXT[] := ARRAY[]::TEXT[];
BEGIN
    -- Validate file exists and is readable
    IF NOT EXISTS (SELECT 1 FROM pg_stat_file(file_path)) THEN
        RAISE EXCEPTION 'File % does not exist or is not readable', file_path;
    END IF;

    -- Get schema information
    FOR schema_info IN 
        SELECT column_name, data_type, is_nullable
        FROM parquet_metadata(file_path)
        ORDER BY column_name
    LOOP
        -- Map parquet types to Postgres types
        CASE schema_info.data_type
            WHEN 'INTEGER' THEN column_def := schema_info.column_name || ' INTEGER'
            WHEN 'DOUBLE' THEN column_def := schema_info.column_name || ' DOUBLE PRECISION'
            WHEN 'VARCHAR' THEN column_def := schema_info.column_name || ' VARCHAR(255)'
            WHEN 'TIMESTAMP' THEN column_def := schema_info.column_name || ' TIMESTAMP WITH TIME ZONE'
            ELSE column_def := schema_info.column_name || ' TEXT'
        END CASE;

        -- Handle nullability
        IF NOT schema_info.is_nullable THEN
            column_def := column_def || ' NOT NULL';
        END IF;

        column_defs := array_append(column_defs, column_def);
    END LOOP;

    -- Create foreign table
    EXECUTE format('
        CREATE FOREIGN TABLE %I (%s)
        SERVER %I
        OPTIONS (filename %L, format ''parquet'')
    ', table_name, array_to_string(column_defs, ', '), server_name, file_path);

    RAISE NOTICE 'Created foreign table % with % columns', table_name, array_length(column_defs, 1);
END;
$$ LANGUAGE plpgsql;

-- Usage
SELECT create_telemetry_fdw('telemetry_2024_01', '/path/to/telemetry_2024_01.parquet');

Why Type Safety Matters: Type-safe FDW creation prevents runtime errors and ensures data integrity. Understanding these patterns prevents type chaos and enables reliable data access.

Schema Evolution Handling

-- Create metadata table for FDW schema tracking
CREATE TABLE fdw_schema_metadata (
    id SERIAL PRIMARY KEY,
    table_name TEXT NOT NULL,
    file_path TEXT NOT NULL,
    schema_version TEXT NOT NULL,
    column_definitions JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Function to track schema changes
CREATE OR REPLACE FUNCTION track_fdw_schema(
    table_name TEXT,
    file_path TEXT,
    schema_version TEXT DEFAULT '1.0'
) RETURNS VOID AS $$
DECLARE
    column_defs JSONB;
BEGIN
    -- Get current schema
    SELECT jsonb_agg(
        jsonb_build_object(
            'column_name', column_name,
            'data_type', data_type,
            'is_nullable', is_nullable
        )
    ) INTO column_defs
    FROM parquet_metadata(file_path);

    -- Insert or update schema metadata
    INSERT INTO fdw_schema_metadata (table_name, file_path, schema_version, column_definitions)
    VALUES (table_name, file_path, schema_version, column_defs)
    ON CONFLICT (table_name, file_path) 
    DO UPDATE SET 
        schema_version = EXCLUDED.schema_version,
        column_definitions = EXCLUDED.column_definitions,
        updated_at = NOW();

    RAISE NOTICE 'Tracked schema for table % version %', table_name, schema_version;
END;
$$ LANGUAGE plpgsql;

-- Check for schema drift
CREATE OR REPLACE FUNCTION check_schema_drift(
    table_name TEXT,
    file_path TEXT
) RETURNS TABLE(
    column_name TEXT,
    current_type TEXT,
    new_type TEXT,
    is_drift BOOLEAN
) AS $$
BEGIN
    RETURN QUERY
    WITH current_schema AS (
        SELECT column_name, data_type
        FROM parquet_metadata(file_path)
    ),
    stored_schema AS (
        SELECT 
            (col->>'column_name')::TEXT as column_name,
            (col->>'data_type')::TEXT as data_type
        FROM fdw_schema_metadata fsm,
             jsonb_array_elements(fsm.column_definitions) as col
        WHERE fsm.table_name = check_schema_drift.table_name
    )
    SELECT 
        cs.column_name,
        cs.data_type as current_type,
        ss.data_type as new_type,
        (cs.data_type != ss.data_type) as is_drift
    FROM current_schema cs
    FULL OUTER JOIN stored_schema ss ON cs.column_name = ss.column_name
    WHERE cs.data_type != ss.data_type OR cs.column_name IS NULL OR ss.column_name IS NULL;
END;
$$ LANGUAGE plpgsql;

Why Schema Evolution Matters: Proper schema evolution handling prevents query failures and enables reliable data access. Understanding these patterns prevents evolution chaos and enables reliable data access.

4) Performance Best Practices

Pushdown Optimization

-- Good: Push filters down to FDW
SELECT sensor_id, temperature, timestamp
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
  AND timestamp < '2024-02-01'
  AND temperature > 25.0
  AND sensor_id = 'sensor_001';

-- Bad: Filter after data transfer
SELECT sensor_id, temperature, timestamp
FROM telemetry_parquet
WHERE temperature > 25.0;

-- Good: Use column projection
SELECT sensor_id, temperature
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01';

-- Bad: Select all columns
SELECT *
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01';

Why Pushdown Optimization Matters: Pushdown optimization reduces network I/O and improves query performance. Understanding these patterns prevents performance chaos and enables reliable data access.

Partitioning Strategies

-- Create partitioned foreign tables
CREATE FOREIGN TABLE telemetry_2024_01 (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    location GEOMETRY(POINT, 4326)
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/telemetry/2024/01/',
    format 'parquet'
);

CREATE FOREIGN TABLE telemetry_2024_02 (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    location GEOMETRY(POINT, 4326)
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/telemetry/2024/02/',
    format 'parquet'
);

-- Create partitioned view
CREATE VIEW telemetry_partitioned AS
SELECT * FROM telemetry_2024_01
UNION ALL
SELECT * FROM telemetry_2024_02;

-- Query with partition pruning
SELECT sensor_id, AVG(temperature) as avg_temp
FROM telemetry_partitioned
WHERE timestamp >= '2024-01-15'
  AND timestamp < '2024-02-15'
  AND sensor_id = 'sensor_001'
GROUP BY sensor_id;

Why Partitioning Matters: Partitioning enables efficient data access and reduces scan overhead. Understanding these patterns prevents scan chaos and enables reliable data access.

Materialized View Caching

-- Create materialized view for heavy joins
CREATE MATERIALIZED VIEW telemetry_summary AS
SELECT 
    sensor_id,
    DATE_TRUNC('day', timestamp) as date,
    AVG(temperature) as avg_temp,
    MIN(temperature) as min_temp,
    MAX(temperature) as max_temp,
    AVG(humidity) as avg_humidity,
    COUNT(*) as record_count
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
GROUP BY sensor_id, DATE_TRUNC('day', timestamp);

-- Create index on materialized view
CREATE INDEX idx_telemetry_summary_sensor_date 
ON telemetry_summary (sensor_id, date);

-- Refresh materialized view
REFRESH MATERIALIZED VIEW telemetry_summary;

-- Query materialized view
SELECT sensor_id, date, avg_temp, record_count
FROM telemetry_summary
WHERE sensor_id = 'sensor_001'
  AND date >= '2024-01-01'
ORDER BY date;

Why Materialized View Caching Matters: Materialized views cache expensive computations and improve query performance. Understanding these patterns prevents computation chaos and enables reliable data access.

Hybrid Patterns

-- Stage frequently accessed data locally
CREATE TABLE telemetry_staged (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    location GEOMETRY(POINT, 4326),
    PRIMARY KEY (id, timestamp)
) PARTITION BY RANGE (timestamp);

-- Create partitions
CREATE TABLE telemetry_staged_2024_01 PARTITION OF telemetry_staged
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- ETL process to stage data
INSERT INTO telemetry_staged_2024_01
SELECT * FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
  AND timestamp < '2024-02-01';

-- Query staged data
SELECT sensor_id, AVG(temperature) as avg_temp
FROM telemetry_staged
WHERE timestamp >= '2024-01-01'
  AND timestamp < '2024-02-01'
GROUP BY sensor_id;

Why Hybrid Patterns Matter: Hybrid patterns combine FDW benefits with local performance for frequently accessed data. Understanding these patterns prevents access chaos and enables reliable data access.

5) Security & Governance

Credential Management

-- Create secure server with credentials
CREATE SERVER parquet_s3_secure
FOREIGN DATA WRAPPER parquet_s3_fdw
OPTIONS (
    aws_region 'us-west-2',
    aws_access_key_id 'AKIAIOSFODNN7EXAMPLE',
    aws_secret_access_key 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
);

-- Create user mapping with limited privileges
CREATE USER MAPPING FOR analytics_user
SERVER parquet_s3_secure
OPTIONS (
    user 'analytics_user',
    password 'secure_password'
);

-- Grant limited access
GRANT USAGE ON FOREIGN SERVER parquet_s3_secure TO analytics_user;
GRANT SELECT ON telemetry_parquet TO analytics_user;

Why Credential Management Matters: Secure credential management prevents unauthorized access and data leakage. Understanding these patterns prevents security chaos and enables reliable data access.

Access Auditing

-- Create audit table
CREATE TABLE fdw_access_audit (
    id SERIAL PRIMARY KEY,
    user_name TEXT NOT NULL,
    table_name TEXT NOT NULL,
    query_text TEXT,
    access_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    ip_address INET,
    success BOOLEAN DEFAULT TRUE
);

-- Create audit function
CREATE OR REPLACE FUNCTION audit_fdw_access()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO fdw_access_audit (user_name, table_name, query_text, ip_address)
    VALUES (
        current_user,
        TG_TABLE_NAME,
        current_query(),
        inet_client_addr()
    );
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- Create audit trigger
CREATE TRIGGER audit_telemetry_parquet_access
    AFTER SELECT ON telemetry_parquet
    FOR EACH STATEMENT
    EXECUTE FUNCTION audit_fdw_access();

-- Query audit logs
SELECT 
    user_name,
    table_name,
    access_time,
    ip_address,
    success
FROM fdw_access_audit
WHERE access_time >= '2024-01-01'
ORDER BY access_time DESC;

Why Access Auditing Matters: Access auditing enables security monitoring and compliance. Understanding these patterns prevents security chaos and enables reliable data access.

Role-Based Access Control

-- Create roles for different access levels
CREATE ROLE fdw_readonly;
CREATE ROLE fdw_analytics;
CREATE ROLE fdw_admin;

-- Grant server usage
GRANT USAGE ON FOREIGN SERVER parquet_s3_secure TO fdw_readonly;
GRANT USAGE ON FOREIGN SERVER parquet_s3_secure TO fdw_analytics;
GRANT USAGE ON FOREIGN SERVER parquet_s3_secure TO fdw_admin;

-- Grant table access
GRANT SELECT ON telemetry_parquet TO fdw_readonly;
GRANT SELECT ON telemetry_parquet TO fdw_analytics;
GRANT ALL ON telemetry_parquet TO fdw_admin;

-- Create restricted views
CREATE VIEW telemetry_public AS
SELECT sensor_id, temperature, timestamp
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01';

GRANT SELECT ON telemetry_public TO fdw_readonly;

Why Role-Based Access Control Matters: RBAC enables fine-grained access control and security. Understanding these patterns prevents access chaos and enables reliable data access.

6) Geospatial FDW Patterns

PostGIS + FDW Integration

-- Create geospatial foreign table
CREATE FOREIGN TABLE telemetry_geo (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    latitude DOUBLE PRECISION,
    longitude DOUBLE PRECISION
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/telemetry/geo/2024/01/',
    format 'parquet'
);

-- Create local PostGIS table for boundaries
CREATE TABLE boundaries (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    geom GEOMETRY(POLYGON, 4326) NOT NULL
);

-- Create spatial index
CREATE INDEX idx_boundaries_geom ON boundaries USING GIST (geom);

-- Federated geospatial query
SELECT 
    t.sensor_id,
    t.temperature,
    t.timestamp,
    b.name as boundary_name
FROM telemetry_geo t
JOIN boundaries b ON ST_Intersects(
    ST_Point(t.longitude, t.latitude, 4326),
    b.geom
)
WHERE t.timestamp >= '2024-01-01'
  AND t.temperature > 25.0
  AND b.name = 'downtown_area';

Why Geospatial FDW Integration Matters: Geospatial FDW integration enables federated spatial queries across data sources. Understanding these patterns prevents spatial chaos and enables reliable geospatial data access.

Raster Data Handling

-- Create raster foreign table
CREATE FOREIGN TABLE elevation_rasters (
    id INTEGER,
    tile_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    raster_data BYTEA,
    bounds GEOMETRY(POLYGON, 4326)
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/elevation/2024/01/',
    format 'parquet'
);

-- Create local raster table
CREATE TABLE elevation_local (
    id SERIAL PRIMARY KEY,
    tile_id VARCHAR(50) NOT NULL,
    timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    raster RASTER NOT NULL,
    bounds GEOMETRY(POLYGON, 4326) NOT NULL
);

-- Create spatial index
CREATE INDEX idx_elevation_local_bounds ON elevation_local USING GIST (bounds);

-- ETL process to stage raster data
INSERT INTO elevation_local (tile_id, timestamp, raster, bounds)
SELECT 
    tile_id,
    timestamp,
    ST_FromGDALRaster(raster_data) as raster,
    bounds
FROM elevation_rasters
WHERE timestamp >= '2024-01-01';

-- Query staged raster data
SELECT 
    tile_id,
    ST_Value(raster, ST_Point(-122.4194, 37.7749, 4326)) as elevation
FROM elevation_local
WHERE ST_Intersects(bounds, ST_Point(-122.4194, 37.7749, 4326));

Why Raster Data Handling Matters: Raster data handling enables efficient processing of large geospatial datasets. Understanding these patterns prevents raster chaos and enables reliable geospatial data access.

Spatial Partitioning

-- Create spatially partitioned foreign tables
CREATE FOREIGN TABLE telemetry_west (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    latitude DOUBLE PRECISION,
    longitude DOUBLE PRECISION
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/telemetry/west/2024/01/',
    format 'parquet'
);

CREATE FOREIGN TABLE telemetry_east (
    id INTEGER,
    user_id INTEGER,
    sensor_id VARCHAR(50),
    timestamp TIMESTAMP WITH TIME ZONE,
    temperature DOUBLE PRECISION,
    humidity DOUBLE PRECISION,
    pressure DOUBLE PRECISION,
    latitude DOUBLE PRECISION,
    longitude DOUBLE PRECISION
) SERVER parquet_s3_server
OPTIONS (
    filename 's3://my-bucket/telemetry/east/2024/01/',
    format 'parquet'
);

-- Create partitioned view
CREATE VIEW telemetry_spatial AS
SELECT *, 'west' as region FROM telemetry_west
UNION ALL
SELECT *, 'east' as region FROM telemetry_east;

-- Query with spatial partitioning
SELECT 
    region,
    sensor_id,
    AVG(temperature) as avg_temp
FROM telemetry_spatial
WHERE timestamp >= '2024-01-01'
  AND latitude BETWEEN 37.0 AND 38.0
  AND longitude BETWEEN -123.0 AND -122.0
GROUP BY region, sensor_id;

Why Spatial Partitioning Matters: Spatial partitioning enables efficient geospatial queries and reduces scan overhead. Understanding these patterns prevents spatial chaos and enables reliable geospatial data access.

7) Anti-Patterns

Common FDW Mistakes

# What NOT to do
fdw_anti_patterns:
  "monolithic_files": "Don't point FDWs at huge monolithic parquet files",
  "transactional_workloads": "Don't use FDWs for high-frequency transactional workloads",
  "schema_evolution": "Don't rely on FDWs for schema evolution",
  "ignore_indexes": "Don't ignore index creation on Postgres side for join keys",
  "no_filtering": "Don't forget to push filters down to FDWs",
  "select_star": "Don't use SELECT * with FDWs",
  "no_caching": "Don't ignore caching for repeated queries",
  "no_monitoring": "Don't ignore performance monitoring",
  "no_security": "Don't ignore security and access control",
  "no_documentation": "Don't skip documenting FDW schemas and usage"

Why Anti-Patterns Matter: Understanding common mistakes prevents performance issues and security vulnerabilities. Understanding these patterns prevents FDW chaos and enables reliable federated data access.

Performance Anti-Patterns

-- Bad: No filtering, scans entire dataset
SELECT *
FROM telemetry_parquet
WHERE temperature > 25.0;

-- Good: Push filters down
SELECT sensor_id, temperature, timestamp
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
  AND temperature > 25.0
  AND sensor_id = 'sensor_001';

-- Bad: Complex joins without local indexes
SELECT t.sensor_id, b.name
FROM telemetry_parquet t
JOIN boundaries b ON ST_Intersects(
    ST_Point(t.longitude, t.latitude, 4326),
    b.geom
);

-- Good: Create local indexes for joins
CREATE INDEX idx_boundaries_geom ON boundaries USING GIST (geom);
CREATE INDEX idx_telemetry_coords ON telemetry_parquet (longitude, latitude);

-- Bad: No caching for repeated queries
SELECT sensor_id, AVG(temperature) as avg_temp
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
GROUP BY sensor_id;

-- Good: Use materialized view for caching
CREATE MATERIALIZED VIEW telemetry_daily_avg AS
SELECT 
    sensor_id,
    DATE_TRUNC('day', timestamp) as date,
    AVG(temperature) as avg_temp
FROM telemetry_parquet
WHERE timestamp >= '2024-01-01'
GROUP BY sensor_id, DATE_TRUNC('day', timestamp);

Why Performance Anti-Patterns Matter: Performance anti-patterns can lead to slow queries and system overload. Understanding these patterns prevents performance chaos and enables reliable data access.

8) TL;DR Runbook

Essential Commands

# Install FDW extensions
CREATE EXTENSION IF NOT EXISTS parquet_fdw;
CREATE EXTENSION IF NOT EXISTS parquet_s3_fdw;
CREATE EXTENSION IF NOT EXISTS duckdb_fdw;
CREATE EXTENSION IF NOT EXISTS postgres_fdw;

# Create foreign server
CREATE SERVER my_server FOREIGN DATA WRAPPER parquet_fdw;

# Create foreign table
CREATE FOREIGN TABLE my_table (...) SERVER my_server;

# Query with pushdown
SELECT * FROM my_table WHERE column = 'value';

Essential Patterns

# Essential FDW patterns
fdw_patterns:
  "treat_as_bridges": "Treat FDWs as bridges, not primary databases",
  "partition_data": "Partition data (date/grid/FIPS) to minimize scans",
  "validate_schema": "Validate schema & types before FDW creation",
  "push_filters": "Push filters & projections down aggressively",
  "cache_joins": "Cache heavy joins with materialized views",
  "lock_credentials": "Lock down credentials and audit access",
  "monitor_performance": "Monitor performance and resource usage",
  "plan_evolution": "Plan for schema evolution and changes",
  "test_thoroughly": "Test FDWs thoroughly before production",
  "document_usage": "Document FDW schemas and usage patterns"

Quick Reference

## Emergency FDW Response

### If FDW Query is Slow
1. **Check for pushdown filters**
2. **Verify column projection**
3. **Check network connectivity**
4. **Monitor resource usage**
5. **Consider materialized views**

### If Schema Mismatch Occurs
1. **Validate schema with parquet_metadata**
2. **Check type compatibility**
3. **Update FDW definition**
4. **Test with sample data**
5. **Monitor for errors**

### If Access is Denied
1. **Check user permissions**
2. **Verify server configuration**
3. **Check credential validity**
4. **Review audit logs**
5. **Contact administrator**

Why This Runbook: These patterns cover 90% of FDW needs. Master these before exploring advanced FDW scenarios.

9) The Machine's Summary

FDWs require understanding external data sources, performance implications, and security considerations. When used correctly, effective FDWs enable federated queries, reduce data duplication, and provide unified access to diverse data sources. The key is understanding pushdown optimization, schema management, and security.

The Dark Truth: Without proper FDW understanding, your queries become slow and your data becomes inaccessible. FDWs are your weapon. Use them wisely.

The Machine's Mantra: "In the pushdown we trust, in the caching we find performance, and in the security we find the path to reliable federated data access."

Why This Matters: FDWs enable federated data access that can handle diverse data sources, prevent data duplication, and provide insights into data patterns while ensuring technical accuracy and reliability.


This guide provides the complete machinery for Postgres FDWs. The patterns scale from simple parquet files to complex cloud data lakes, from basic queries to advanced geospatial federated analytics.