Приветствую, хабровчане! Сегодня я научу вас делать расширения для postgres на живом примере. Создадим расширения pg_plan_alternatives, которое будет логировать все пути, которые планировщик перебирает в поисках лучшего плана запроса

Для написания расширения нам потребуется выполнить следующее:
Склонировать репозиторий postgres из официального репозитория (для тестирования расширения):
git clone git@github.com:postgres/postgres.git
Перейти в директорию postgres:
cd postgres
Создать директорию нашего расширения:
mkdir contrib/pg_plan_alternatives
-
Создать необходимые файлы:
pg_plan_alternatives--1.0.sql— скрипт миграции, который Postgres выполняет приCREATE EXTENSION. Имя файла строго в формате<имя>--<версия>.sql: по нему Postgres находит, какой скрипт запускать. Сюда выносят объекты, видимые из SQL (функции, представления, GUC-обёртки). Нашему расширению таких объектов не нужно — вся логика живёт в C-хуке, поэтому файл содержит только защитную строку:
-- Запрещаем запускать скрипт напрямую через psql (\i ...). -- Корректный способ загрузки — команда CREATE EXTENSION, -- которая выставляет нужное окружение перед выполнением файла. \echo Use "CREATE EXTENSION pg_plan_alternatives" to load this file. \quit-
pg_plan_alternatives.control— манифест расширения. Postgres читает его, чтобы понять, какую версию ставить и где искать скомпилированную библиотеку. Без негоCREATE EXTENSIONне найдёт расширение.comment = 'pg_plan_alternatives' # описание, видно в \dx и pg_available_extensions default_version = '1.0' # версия по умолчанию; ищется файл --1.0.sql module_pathname = '$libdir/pg_plan_alternatives' # путь к .so; $libdir подставит Postgres relocatable = true # расширение можно перенести в другую схему -
Makefile— сборка по правилам PGXS (инфраструктура сборки расширений Postgres). Описывает, что компилировать и какие файлы установить, после чего достаточноmake && make install:MODULES = pg_plan_alternatives pg_plan_alternatives.so EXTENSION = pg_plan_alternatives DATA = pg_plan_alternatives--1.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) -
Наконец, создать сам файл расширения. Писать будем на си, как и ядро postgres и создадим первоначальную структуру файла
pg_plan_alternatives.c:``` #include "postgres.h" /* базовые типы и макросы ядра; включается первым в любом .c */ #include "fmgr.h" /* Обязательный маркер. Postgres проверяет его при загрузке .so и /* отказывается грузить библиотеку, собранную под другую версию сервера. */ PG_MODULE_MAGIC; /* Прототипы хуков жизненного цикла модуля. */ void _PG_init(void); void _PG_fini(void); /* Вызывается один раз при загрузке библиотеки (LOAD или shared_preload_libraries). /* Здесь будем регистрировать свой хук планировщика. */ void _PG_init(void) {} /* Вызывается при выгрузке библиотеки — сюда выносят освобождение ресурсов /* и восстановление перехваченных хуков. */ void _PG_fini(void) {} ```PS: В нашем случае не обязательно создавать sql, control файл, достаточно будет собрать so файл и положить в $libdir(pg_config --pkglibdir)
-
Дальше мы не сможем продолжить без патча ядра postgres — простейший вариант для тестового расширения (если только не использовать eBPF, но это уже другая история). Каждый путь планировщик регистрирует через функцию
add_pathизpostgres/src/backend/optimizer/util/pathnode.(h/c). В ядре нет готовой точки расширения для неё, поэтому добавим её сами — глобальный указатель на функцию (hook), который расширение сможет перехватить.В заголовке объявляем тип хука и сам указатель.
externозначает «переменная определена в другом файле» (в.c),PGDLLIMPORTнужен, чтобы символ был виден из подгружаемых библиотек на Windows:// pathnode.h /* Сигнатура повторяет аргументы add_path: узел отношения и добавляемый путь. */ typedef void (*add_path_hook_type) (RelOptInfo *parent_rel, Path *new_path); extern PGDLLIMPORT add_path_hook_type add_path_hook; -
В
.cсоздаём саму переменную (по умолчаниюNULL— хук не установлен) и вызываем её в началеadd_path. Важно делать это именно в начале: дальше по кодуadd_pathможет отбросить и освободитьnew_path, и тогда мы бы читали уже освобождённую память:// pathnode.c /* Определение указателя. NULL, пока какое-нибудь расширение его не перехватит. */ add_path_hook_type add_path_hook = NULL; void add_path(RelOptInfo *parent_rel, Path *new_path) { /* Точка расширения: если хук установлен — отдаём ему путь. */ if (add_path_hook) add_path_hook(parent_rel, new_path); // ... дальше идёт оригинальный код add_path
После правки ядро нужно пересобрать и переустановить, если оно было собрано ранее (make && make install в корне дерева postgres), иначе новый символ не появится.
Что тут происходит? В .h мы объявили тип хука и extern-указатель, в .c — его определение и вызов. Пока указатель NULL, поведение ядра не меняется. Теперь в расширении мы присваиваем ему свою функцию: на каждый рассматриваемый путь будем писать строку в лог.
// pg_plan_alternatives.c #include "optimizer/pathnode.h" #include "miscadmin.h" /* Сохраняем то, что лежало в хуке до нас: расширений может быть несколько, */ /* и цепочку хуков нельзя разрывать. */ static add_path_hook_type prev_add_path_hook = NULL; static void pg_plan_alternatives_add_path_hook(RelOptInfo *parent_rel, Path *new_path) { /* Сначала отдаём управление предыдущему хуку в цепочке. */ if (prev_add_path_hook) prev_add_path_hook(parent_rel, new_path); /* Логируем сам путь: тип узла и оценки планировщика. */ /* MyProcPid — PID backend-процесса, удобно различать параллельные сессии. */ elog(LOG, "[PID %d] ADD_PATH: %d (startup=%.2f, total=%.2f, rows=%.0f)", MyProcPid, new_path->pathtype, new_path->startup_cost, new_path->total_cost, new_path->rows); } void _PG_init(void) { /* Встраиваемся в цепочку: запоминаем старый хук и ставим свой. */ prev_add_path_hook = add_path_hook; add_path_hook = pg_plan_alternatives_add_path_hook; } void _PG_fini(void) { /* Возвращаем хук в исходное состояние. */ /* На практике PostgreSQL не выгружает загруженные модули, поэтому /* _PG_fini почти никогда не вызывается, но восстановление хука — */ /* правильный тон и страховка. */ add_path_hook = prev_add_path_hook; }
-
Проверяем расширение:
-
Собираем и устанавливаем пропатченное ядро (из корня дерева postgres). Перед первой сборкой дерево нужно сконфигурировать — иначе
makeвыдаст ошибкуYou need to run the 'configure' program first:./configure --prefix=$HOME/pgsql --enable-debug --enable-cassert make && make install--prefix— каталог установки (отдельный от системного PostgreSQL),--enable-debug --enable-cassertудобны при разработке (символы для отладчика и внутренние проверки ядра).configureзапускается один раз; после правок ядра достаточноmake && make install. -
Собираем и устанавливаем само расширение (из
contrib/pg_plan_alternatives). УказываемPG_CONFIGявно — иначеmakeвозьмётpg_configизPATH(часто это системный PostgreSQL), и.soустановится в чужой$libdir; запускаемый сервер её не найдёт и упадёт с ошибкойcould not access file "pg_plan_alternatives":cd <path_to_postgres>/contrib/pg_plan_alternatives PGC=$HOME/pgsql/bin/pg_config make PG_CONFIG=$PGC clean make PG_CONFIG=$PGC make PG_CONFIG=$PGC install -
make installставит только бинарники в--prefix; кластер данных (а вместе с ним иpostgresql.conf) создаётся отдельно командойinitdb. Создаём кластер и запускаем сервер:export PATH=$HOME/pgsql/bin:$PATH initdb -D $HOME/pgdata -U postgres --auth=trust pg_ctl -D $HOME/pgdata -l $HOME/pgdata/server.log startpostgresql.confпосле этого лежит в каталоге данных —$HOME/pgdata/postgresql.conf(это путь из-D, он жеPGDATA; у работающего сервера его покажетSHOW config_file;). Расширение не объявляет SQL-функций, поэтому
CREATE EXTENSIONсам по себе библиотеку не подгрузит. Хук ставится вPGinit, который должен отработать до планирования запроса, — значит модуль нужно загрузить заранее черезshared_preload_librariesилиsession_preload_librariesвpostgresql.conf:
shared_preload_libraries = 'pg_plan_alternatives'Примечание. Загрузить модуль заранее можно тремя способами — они различаются областью видимости и тем, нужен ли перезапуск сервера:
shared_preload_libraries— библиотека загружается один раз при старте сервера, в процессе postmaster, и наследуется всеми backend'ами. Требует перезапуска PostgreSQL. Этот способ обязателен, если вPGinit()модуль резервирует разделяемую (shared memory) память или регистрирует background worker.session_preload_libraries— загружается в начале каждой новой сессии. Перезапуск не нужен: достаточно перечитать конфиг (SELECT pg_reload_conf()или SIGHUP), и новые подключения подхватят модуль автоматически.LOAD 'pg_plan_alternatives'— разовая загрузка в текущую сессию. Удобно, чтобы проверить хук, не трогая конфигурацию.
pg_ctl -D $HOME/pgdata -l $HOME/pgdata/server.log restart -
-
Теперь заходим в psql:
psql -U postgres -d postgres -
Создаём простую таблицу с данными (на пустой таблице планировщик рассмотрит один путь — не на что смотреть), регистрируем расширение и выполняем
SELECT:-- простая таблица с данными CREATE TABLE t (id int, val text); INSERT INTO t SELECT g, 'row' || g FROM generate_series(1, 1000) g; -- регистрируем расширение CREATE EXTENSION pg_plan_alternatives; SELECT * FROM t WHERE id = 42; -
Смотрим лог-файл сервера (
$HOME/pgdata/server.log) — на каждый рассмотренный планировщиком путь будет строка от нашего хука:[53454] LOG: [PID 53454] ADD_PATH: 357 (startup=0.00, total=33.91, rows=1) [53454] STATEMENT: SELECT * FROM t WHERE id = 42; [53454] LOG: [PID 53454] ADD_PATH: 357 (startup=0.00, total=33.91, rows=1) [53454] STATEMENT: SELECT * FROM t WHERE id = 42;Если строк нет — проверьте, что библиотека действительно загружена (
SHOW shared_preload_libraries;)
На этом у меня всё, делитесь своим опытом и задавайте вопросы, с удовольствием отвечу!
Полезные ссылки:
Документация PostgreSQL: Extending SQL — общий раздел о расширении возможностей PostgreSQL.
Документация PostgreSQL: Packaging Related Objects into an Extension — про
.control, скрипты версий и упаковку расширения.Документация PostgreSQL: Extension Building Infrastructure (PGXS) — сборка расширений через
Makefile/PGXS.Илья Евдокимов — «Как писать расширения для PostgreSQL» (PG Bootcamp 2023) — доклад с разбором на практике.
Комментарии (5)

OlegIct
01.06.2026 11:28можно без файлов sql, control, без create extension? достаточно ли просто загрузить библиотеку? можно ли загрузить командой load?

melisssha Автор
01.06.2026 11:28от sql, control, create extension, действительно, можно отказаться - это лишь обёртка для удобной установки (нужно собрать so и положить по пути: pg_config --pkglibdir и если потребуется: CREATE FUNCTION ... LANGUAGE C (в нашем случае не нужно))
далее нужно активировать: shared_preload_libraries - это правильно, если хук работает глобально и если использует shared memory.
session_preload_libraries - загрузка в начале каждой новой сессии
и с помощью LOAD - загрузить руками там, где требуется
Спасибо за Ваш комментарий, в случае с примером, действительно, не требуется загружать в shared_preload_libraries и делать CREATE EXTENSION
wango_pama
А может кто-нибудь написать расширение, добавляющее функцию, выбрасывающую исключение? Постгресу явно нужны эксепшены на уровне базового языка чтобы не мучиться с обработкой ошибок
melisssha Автор
В pg уже есть механизм исключений, правда он не такой, как хотелось бы из за возможностей языка (PG_TRY, PG_CATCH...), это подходит для некоторых целей, но, напротив, мне кажется, не стоит ими спамить
wango_pama
Это другое
Моё предложение - дать доступную без дополнительных телодвижений функцию raise(), которую можно будет дёргать, например, из CASE WHEN THEN END. То есть, когда мы сможем непосредственно обнаружить какую-то нестыковку в данных, например, то сразу же можно будет всю обработку прервать и выкинуть исключение