惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Proofpoint News Feed
D
DataBreaches.Net
D
Docker
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
J
Java Code Geeks
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
Martin Fowler
Martin Fowler
U
Unit 42
Engineering at Meta
Engineering at Meta
IT之家
IT之家
Vercel News
Vercel News
B
Blog RSS Feed
人人都是产品经理
人人都是产品经理
博客园 - Franky
博客园 - 【当耐特】
Stack Overflow Blog
Stack Overflow Blog
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Как написать свое расширение postgres?
prankware · 2026-06-01 · via Все публикации подряд на Хабре

Средний

7 мин

6.6K

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

Просто классный слон, postgres же, а почему ты смотришь на эту надпись? Читай статью

Просто классный слон, postgres же, а почему ты смотришь на эту надпись? Читай статью

Для написания расширения нам потребуется выполнить следующее:

  • Склонировать репозиторий 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;
}
  • Проверяем расширение:

    1. Собираем и устанавливаем пропатченное ядро (из корня дерева 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.

    2. Собираем и устанавливаем само расширение (из 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
    3. 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 start
      

      postgresql.conf после этого лежит в каталоге данных — $HOME/pgdata/postgresql.conf (это путь из -D, он же PGDATA; у работающего сервера его покажет SHOW config_file;).

    4. Расширение не объявляет 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;)

На этом у меня всё, делитесь своим опытом и задавайте вопросы, с удовольствием отвечу!

Полезные ссылки: