PostgreSQL давно умеет превращать кластер в федеративный узел: через Foreign Data Wrapper (FDW) можно прозрачно читать и писать в удалённые базы, объединять их в одном запросе, агрегировать и фильтровать. Ключ к производительности здесь — pushdown: способность протолкнуть фильтры, сортировки, join’ы и агрегации на удалённый сервер, чтобы передавать по сети минимум строк и промежуточных данных. В этой статье на практике разбираем, как FDW работает, как правильно настроить postgres_fdw, что именно может быть протолкнуто вниз, как убедиться в pushdown через EXPLAIN и как устранить типичные препятствия. Если разворачиваете отдельные кластеры под отчётность — удобнее делать это на изолированных виртуальных серверах, например на облачных VDS.
Что такое FDW и почему важен pushdown
FDW — это интерфейс, позволяющий PostgreSQL обращаться к внешним источникам данных так, как будто это локальные таблицы. Для подключения к другим серверам PostgreSQL используется модуль postgres_fdw. Сценарии — от миграций и разбиения монолитной БД на несколько шардов до построения витрин и интеграции разнородных систем.
Без pushdown сервер-инициатор вынужден вытянуть с удалённой стороны много строк и уже локально фильтровать, сортировать, джойниться и агрегировать. Это упирается в сеть, потребляет память и CPU, а главное — масштабируется плохо. Pushdown переносит вычисления на удалённый сервер: он возвращает только финальный, уже отсортированный или агрегированный набор, минимизируя сетевые пересылки и увеличивая общую пропускную способность федеративного контура.
Базовая настройка postgres_fdw
Далее — короткая пошаговая настройка. Предполагается, что у вас есть доступ к обоим кластерам PostgreSQL и учётная запись на удалённом сервере с нужными правами чтения/записи.
-- 1) На локальном сервере
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- 2) Описываем удалённый сервер
CREATE SERVER reporting_srv
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '10.0.2.15', port '5432', dbname 'reporting');
-- 3) Маппим локального пользователя на удалённого
CREATE USER MAPPING FOR app
SERVER reporting_srv
OPTIONS (user 'fdw_reader', password 'secret');
-- 4) Импортируем схему либо создаём внешние таблицы вручную
IMPORT FOREIGN SCHEMA public
FROM SERVER reporting_srv
INTO fdw_public;
-- или пример явного описания
CREATE FOREIGN TABLE fdw_public.orders (
id bigint,
customer_id bigint,
total numeric,
created_at timestamp without time zone
) SERVER reporting_srv
OPTIONS (schema_name 'public', table_name 'orders');
Минимальной конфигурации достаточно для первого запроса. Но ради производительности нам понадобится тонкая настройка и понимание, что именно может быть протолкнуто на удалённую сторону.
Как понять, что pushdown работает
Ваш лучший друг — EXPLAIN (VERBOSE, ANALYZE, BUFFERS). Для внешних таблиц вы увидите узел Foreign Scan, а в детальной секции — строку Remote SQL. Если оператор действительно протолкнут, Remote SQL будет содержать WHERE, JOIN, GROUP BY, ORDER BY и т. п.
EXPLAIN (VERBOSE, ANALYZE, COSTS OFF)
SELECT id, total
FROM fdw_public.orders
WHERE created_at >= now() - interval '7 days'
ORDER BY total DESC
LIMIT 10;
В идеале вы увидите нечто вроде:
Foreign Scan on fdw_public.orders
Remote SQL: SELECT id, total FROM public.orders
WHERE (created_at >= (now() - '7 days'::interval))
ORDER BY total DESC
LIMIT 10
Если Remote SQL не содержит фильтров или сортировок — pushdown не сработал, и это отправная точка для разбирательства.
Что умеет pushdown в postgres_fdw
Современные версии postgres_fdw поддерживают проталкивание большого набора операций:
- Фильтры
WHERE, включая простые выражения и многие функции/операторы PostgreSQL. - Сортировки
ORDER BYи ограничение результатаLIMIT/OFFSET. - Агрегации
GROUP BY,HAVING, агрегатные функции. - JOIN на удалённой стороне, если обе внешние таблицы ссылаются на один и тот же сервер FDW.
- Параметризованные пути (correlated subqueries, semi-join/anti-join), когда это выгодно планировщику.
- Запись:
INSERT,UPDATE,DELETE— сами DML исполняются удалённо; могут поддерживаться батчи.
Но у каждого из пунктов есть условия. Дальше — нюансы и примеры.

Фильтры WHERE и функции: immutable/stable и коллаторы
Фильтры хорошо проталкиваются, если выражения «переносимы» на удалённый сервер: он должен понимать задействованные операторы и функции. Для встроенных операторов это почти всегда так. Препятствия типичны:
- VOLATILE-функции часто не шиппятся.
now()иcurrent_date— STABLE, обычно допускаются, ноrandom()— VOLATILE и, скорее всего, останется локально. - Коллации. Если локальная и удалённая стороны используют разные коллации для текстов,
ORDER BYили сравнения строк могут не быть протолкнуты. - Пользовательские функции/операторы из расширений по умолчанию «неизвестны» FDW. Их можно разрешить, указав список шиппабельных расширений в опции сервера.
Пример явного разрешения функций из расширений для шиппинга:
ALTER SERVER reporting_srv OPTIONS (add extensions 'citext');
После этого функции и операторы из citext станут кандидатами для pushdown, если остальное совместимо.
Диагностика через EXPLAIN
Если фильтр не уехал на удалённый сервер, EXPLAIN покажет локальные узлы фильтрации над Foreign Scan, а Remote SQL будет «голым». Нередко виновата простая вещь: другая коллация, не-IMMUTABLE функция или смешение типов, заставляющее локально выполнять приведение.
JOIN pushdown: когда джойнить удалённо
FDW может выполнить JOIN на удалённой стороне, если:
- Обе внешние таблицы ссылаются на один и тот же объект
SERVERи, соответственно, одно подключение. - Условия соединения и фильтры шиппабельны (нет несовместимых коллаций, «левых» функций и прочего).
- Нет барьеров безопасности, мешающих перемещению выражений (например, security barrier у представлений).
EXPLAIN (VERBOSE, COSTS OFF)
SELECT o.id, c.email, o.total
FROM fdw_public.orders o
JOIN fdw_public.customers c ON c.id = o.customer_id
WHERE o.created_at >= now() - interval '30 days';
Идеальный случай:
Foreign Scan
Remote SQL: SELECT r1.id, r2.email, r1.total
FROM public.orders r1
JOIN public.customers r2 ON r2.id = r1.customer_id
WHERE (r1.created_at >= (now() - '30 days'::interval))
Если таблицы на разных серверах FDW — полного pushdown JOIN не будет. Планировщик протолкнёт фильтры на каждую сторону, заберёт укороченные выборки и соединит локально. В этом случае критичны селективность фильтров и ширина строк.
Агрегации, сортировки и LIMIT
Агрегации и ORDER BY ... LIMIT обычно отлично шиппятся. Если вы видите в Remote SQL GROUP BY и агрегатные функции — это то, что нужно: удалённый сервер вернёт уже компактный агрегированный набор.
EXPLAIN (VERBOSE, COSTS OFF)
SELECT customer_id, sum(total) AS sum_total
FROM fdw_public.orders
WHERE created_at >= date_trunc('month', now())
GROUP BY customer_id
HAVING sum(total) > 1000
ORDER BY sum_total DESC
LIMIT 20;
Если сортировка или группировка осталась локально — проверьте коллации и совместимость типов. Иногда помогает явное COLLATE "C" для технической сортировки по байтам (если это допустимо по бизнес-логике).
Параметризованные пути и коррелированные подзапросы
FDW умеет строить параметризованные планы: когда условие подзапроса зависит от строки внешнего узла. Вы часто увидите, что вместо «крупного запроса» с передачей массива значений на удалённую сторону планировщик выберет серию более маленьких удалённых запросов с параметрами. Это повышает селективность (и шанс pushdown), но добавляет сетевых раунд-трипов. Наблюдайте реальное время по EXPLAIN ANALYZE и корректируйте настройки выборки (fetch_size), чтобы держать баланс.
Запись: INSERT/UPDATE/DELETE и batch
Операции записи в postgres_fdw исполняются удалённо. Важные моменты:
- RETURNING поддерживается: можно вернуть сгенерированные значения, но не все конструкции одинаково быстры.
- Batch. Для массовых вставок и обновлений проверьте опцию батчинга на уровне таблицы/сервера, чтобы сокращать число round-trip’ов.
- Триггеры и ограничения работают на удалённой стороне; учитывайте это при ожиданиях логики.
- Двухфазный коммит. При необходимости атомарности между несколькими удалёнными серверами исследуйте поддержку 2PC и подготовленных транзакций; включайте с осторожностью.
Тонкая настройка производительности
use_remote_estimate: точнее оценка — лучше планы
Планировщик по умолчанию оценивает стоимость удалённых узлов на основе локальной статистики и общих коэффициентов. Сложные комбинации JOIN/GROUP BY оцениваются неточно, и план может быть далёк от оптимального. Опция use_remote_estimate заставляет postgres_fdw запрашивать оценки у удалённого планировщика (выполняется EXPLAIN на удалённой стороне), что сильно повышает качество выбранного плана.
ALTER SERVER reporting_srv OPTIONS (add use_remote_estimate 'true');
-- или на уровне конкретной таблицы
ALTER FOREIGN TABLE fdw_public.orders OPTIONS (add use_remote_estimate 'true');
Цена — дополнительный оверхед на планирование (удалённый EXPLAIN). Имеет смысл на тяжёлых запросах, особенно с join pushdown и агрегациями.
fetch_size: балансируем сетевые раунды
fetch_size определяет размер партии строк за одно чтение курсора. Малое значение — больше раунд-трипов, большое — больше пикового потребления памяти при передаче. Начните с десятков тысяч для аналитических запросов и подберите под нагрузку.
ALTER SERVER reporting_srv OPTIONS (set fetch_size '20000');
ALTER FOREIGN TABLE fdw_public.orders OPTIONS (set fetch_size '50000');
fdw_startup_cost и fdw_tuple_cost
Это глобальные параметры планировщика, влияющие на предпочтения PostgreSQL при выборе между локальными и внешними путями. Если удалённая сторона медленная или сеть узкая, увеличьте их, чтобы планировщик был консервативнее в выборе удалённых путей. И наоборот.
SET fdw_startup_cost = 200.0;
SET fdw_tuple_cost = 0.02;
Меняйте их аккуратно и по месту: лучше на уровне сессии в экспериментальном профилировании.
Анализ и статистика на удалённой стороне
Точность оценок напрямую зависит от актуальности статистики на удалённом сервере: не забывайте про регулярные VACUUM (ANALYZE) и корректную конфигурацию автовакуумера. На локальной стороне вы тоже можете запускать ANALYZE по внешним таблицам — postgres_fdw умеет удалённо самплировать данные и собирать статистику о селективности выражений. Полезно освежить знания по настройке autovacuum и статистики.
ANALYZE fdw_public.orders;

Асинхронность и параллелизм
postgres_fdw умеет асинхронно выполнять несколько внешних сканов, что помогает при обращении к нескольким серверам или к нескольким внешним таблицам. Также возможны параллельные планы, если узел допускает этот режим. Помните: каждый параллельный воркер — это отдельное соединение к удалённой стороне.
Сетевые и сессионные параметры
В объекте SERVER разрешены параметры подключения libpq: keepalives, keepalives_idle, keepalives_interval, application_name, sslmode и т. д. Их стоит использовать для стабильности соединений и слежения за активностью.
ALTER SERVER reporting_srv OPTIONS (
set application_name 'fdw-reporting',
set keepalives '1',
set keepalives_idle '30',
set keepalives_interval '10'
);
Полезно прокидывать на удалённую сторону сессионные GUC, например statement_timeout, чтобы тяжёлые запросы не зависали бесконечно:
ALTER USER MAPPING FOR app SERVER reporting_srv
OPTIONS (add options '-c statement_timeout=60000');
Типичные препятствия pushdown и как их обойти
Разные коллации и сопоставление текстов
Если локальный сервер инициализирован с одной коллацией, а удалённый — с другой, сортировки и часть сравнений строк могут остаться локально. Что делать:
- Убедиться, что «бизнесовые» сортировки совпадают. Если нет — возможно, локальная сортировка оправдана и неизбежна.
- Если нужна байтовая сортировка, явно указывайте
COLLATE "C"в запросах — тогдаORDER BYчаще протолкнётся.
Функции из расширений
Когда в фильтре используется функция из расширения, которое есть на обоих серверах, по умолчанию она может считаться «недоставляемой». Добавьте расширение в белый список опцией extensions у SERVER. Проверяйте EXPLAIN после изменения.
VOLATILE-выражения
Избегайте random(), clock_timestamp() и других VOLATILE-функций в фильтрах по внешним таблицам. Если логика допускает, замените на STABLE аналоги или вычислите значение заранее в переменной/CTE и используйте как константу запроса.
Security barrier и представления
Представления, помеченные как security-barrier, могут блокировать проталкивание выражений и соединений через них. По возможности джойнтесь с «сырыми» внешними таблицами либо переписывайте запрос так, чтобы условия оказались ближе к источнику.
Federated queries на нескольких серверах
Когда запрос соединяет данные с двух разных удалённых серверов, полноценный join pushdown невозможен: каждый удалённый узел получит свои фильтры и вернёт укороченный набор, а объединение произойдёт локально. Практические советы:
- Обеспечьте максимально селективные фильтры на каждой стороне — используйте индексы, статистику и
use_remote_estimate. - Сокращайте ширину строк (выбирайте только нужные столбцы), чтобы меньше данных гонять по сети.
- Если есть «мастер»-справочники, подумайте о кэшировании подмножеств локально (материализованные представления) с регламентным обновлением.
Пулеры соединений и FDW
FDW держит сессию с удалённой стороной, в которой готовит запросы и может использовать prepared statements. Пулеры транзакционного уровня могут ломать эти предположения, приводя к ошибкам вида «prepared statement does not exist» или к деградации от повторной подготовки. По возможности используйте пулирование на уровне сессии для соединений, через которые идут FDW-запросы, либо настройте исключения/отдельные пулы под FDW.
Надёжность и транзакции
По умолчанию транзакции через FDW не гарантируют атомарность между несколькими удалёнными серверами. Если бизнес-кейс требует межсерверной атомарности, изучите двухфазный коммит: включение соответствующих опций и разрешение prepared transactions на удалённой стороне. Помните про риски 2PC (зависшие prepared transactions) и контролируйте их регламентной очисткой и мониторингом.
Наблюдаемость и отладка
- EXPLAIN с
VERBOSE— чтобы видетьRemote SQLи реальную картину pushdown. - pg_stat_statements на обеих сторонах — локально видите обёрточные запросы, удалённо — реальные запросы, приходящие от FDW.
- application_name на
SERVER— облегчает фильтрацию сессий вpg_stat_activityи логах удалённого сервера. - Для метрик используйте сбор на удалённой стороне и экспорт. Подробности — в заметке о сборе метрик в Prometheus с remote_write.
Пошаговый чек-лист для ускорения
- Сделайте
EXPLAIN (VERBOSE, ANALYZE)и проверьте, что нужные узлы есть вRemote SQL. Если их нет — ищите несовместимости (коллации, функции). - Включите
use_remote_estimateдля тяжёлых запросов и убедитесь, что на удалённой стороне актуальная статистика. - Подберите
fetch_size: меньше раундов, но не за счёт переполнения памяти и длинных пауз. - Убедитесь, что обе стороны используют совместимые расширения; добавьте их в опцию
extensionsу сервера FDW, если нужно. - Сведите типовые преобразования к минимуму: приводите типы явно, избегайте скрытых cast’ов.
- Для federated queries с несколькими серверами переносите фильтры к источникам, уменьшайте набор столбцов и ширину строк.
- Пересмотрите архитектуру долгих конвейеров: иногда проще материализовать промежуточные результаты с регламентным обновлением.
Борьба с «странными» случаями
Несколько характерных ситуаций, из-за которых pushdown внезапно «ломается»:
- Сравнение по разным типам:
text = uuidс неявным кастом. Явно приведите типы, чтобы удалённая сторона могла построить индексное условие и увести фильтр вниз. - JSONB-функции: многие встроенные операции по JSONB шиппятся, но если используете функции из стороннего расширения — проверьте whitelisting через
extensions. - CTE: в новых версиях CTE не всегда барьер оптимизации, но сложные WITH-цепочки могут мешать pushdown. Попробуйте переписать на подзапросы или инлайнить куски.
- SECURITY DEFINER функции: иногда тянут за собой запрет на перемещение выражений. Тестируйте EXPLAIN’ом.
Итоги
Сильная сторона postgres_fdw — не просто прозрачный доступ к удалённым таблицам, а именно эффективное распределённое выполнение: фильтры, сортировки, агрегаты и соединения выполняются там, где данные лежат. Это и есть pushdown. Чтобы он стабильно работал и давал выигрыш, уделите внимание:
- Единству коллаций и типов, whitelisting расширений, аккуратному обращению с функциями.
- Точности оценок:
use_remote_estimate, актуальная статистика и ANALYZE на обеих сторонах (см. также тюнинг autovacuum). - Сетевым батчам:
fetch_sizeи настройкам keepalive. - Архитектуре федеративных запросов: селективность, ширина строк и материализация там, где это дешевле.
Когда всё это сходится, federated queries превращаются из «удобной трюковой штуки» в предсказуемый и быстрый инструмент. А EXPLAIN (VERBOSE) — ваш главный индикатор того, что pushdown действительно работает.


