Назад к блогу

Как устроен PostgreSQL: что сделало его самой популярной открытой СУБД

Как устроен PostgreSQL: что сделало его самой популярной открытой СУБД

PostgreSQL часто называют самой популярной открытой СУБД, но за этим статусом стоит конкретная инженерная архитектура — набор взаимодействующих процессов, собственная модель памяти и механизм журналирования WAL. В статье разбирается, как эти решения обеспечивают надёжность и расширяемость, чем подход PostgreSQL отличается от MySQL и SQL Server и как понимание внутреннего устройства помогает на практике диагностировать узкие места и настраивать базу.

Архитектура процессов: почему PostgreSQL работает как набор независимых сервисов

PostgreSQL — не «одна большая программа», а набор скоординированных операционных процессов. Когда клиент открывает соединение, супервизор postgres порождает отдельный backend-процесс под это соединение. Дальше каждый backend работает со своей копией памяти, а за согласованность отвечает общая разделяемая память.

Разделяемая память устроена так:

  • shared buffers — кэш страниц таблиц и индексов. Это главный кэш; всё, что читается из диска, проходит через него.
  • WAL buffers — буфер журнала предварительной записи (Write-Ahead Log). Любое изменение сначала попадает сюда, потом в файлы WAL.
  • clog, lock-таблицы, статистика — служебные структуры, которые нужны всем backend-процессам одновременно.

Рядом живут фоновые процессы. checkpointer периодически сбрасывает «грязные» страницы из shared buffers на диск и фиксирует точку восстановления (checkpoint). background writer разгружает checkpointer, заранее выталкивая страницы под нагрузкой. walwriter пишет WAL-буферы в файлы журнала. Ещё есть autovacuum launcher, logical replication launcher, archiver — у каждого своя зона ответственности.

Ключевая идея — правило WAL: журнал на диск должен попасть раньше, чем изменённая страница данных. Представьте перевод денег между счетами. Backend меняет страницы в shared buffers и одновременно пишет запись в WAL: что и где изменилось. Транзакция считается зафиксированной, когда WAL-запись сброшена на диск. Даже если питание пропадёт сразу после этого, при старте PostgreSQL прочитает журнал и повторит изменения. Отсюда и свойство durability — «D» из ACID.

Почему такое устройство важно? Оно объясняет две вещи, за которые PostgreSQL ценят в эксплуатации. Первая — надёжность восстановления: система знает точку, с которой можно переиграть журнал. Вторая — расширяемость: WAL нужен не только для краха, но и для физической репликации и для Point-in-Time Recovery. В документации это описано как основа надёжности и репликации.

Отличия от других СУБД проявляются здесь же:

  • Соединение на процесс. У MySQL или SQL Server модель ближе к «поток на соединение». Отсюда практическое следствие: тысячи одновременных подключений к PostgreSQL дорого стоят по памяти, и вместо них ставят пул вроде pgbouncer.
  • MVCC в самом ядре. PostgreSQL не блокирует читателей ради пишущих: каждая транзакция видит свой снимок данных. Поэтому «грязные» строки не удаляются мгновенно, а ждут очистки — этим занимается autovacuum.
  • Единый WAL для всего. Один механизм обслуживает и восстановление, и репликацию, и архивирование. В ряде СУБД это отдельные подсистемы.

Понимание этой схемы упрощает диагностику. Растёт shared_buffers — следите за памятью; копится WAL — проверьте частоту checkpoint; тормозит очистка — настройте autovacuum. Всё это следствия одной и той же архитектуры.

Модель данных: объектно-реляционный подход и его практические преимущества

SQL-таблицы не обязаны состоять только из чисел и строк. В PostgreSQL столбец может хранить массив, JSON-документ, диапазон или даже составной тип, который вы определили сами. Это и есть объектно-реляционная модель: реляционная основа (строки, столбцы, транзакции) плюс объектные расширения (пользовательские типы, наследование таблиц, перегрузка операторов, функции над типами).

Начнём с пользовательских типов. Команда CREATE TYPE даёт два формата. Первый — составной тип: набор именованных полей, по сути строка внутри строки.

CREATE TYPE address AS (
    city    text,
    street  text,
    zip     text
);

Такой тип можно использовать как обычный столбец. Внутри address поля читаются через точку: (addr).city. Значение проверяется на уровне типа, а не отдельными CHECK-констрейнтами на каждое поле.

Второй формат — перечисление (ENUM): фиксированный список допустимых значений. Статус заказа удобно описать так:

CREATE TYPE order_status AS ENUM ('new', 'paid', 'shipped', 'cancelled');

Сервер отклонит любое значение вне списка. Важная деталь: ENUM хранится компактно (4 байта на значение) и сортируется по порядку объявления, а не по алфавиту. Добавить новое значение можно через ALTER TYPE ... ADD VALUE, но вставка в середину списка или удаление значения потребует пересоздания типа — это ограничение стоит учитывать при проектировании.

Дальше — массивы. Массивный столбец избавляет от вспомогательной таблицы, когда связь «один ко многим» невелика и не требует отдельных индексов. Например, теги поста:

CREATE TABLE posts (
    id   serial PRIMARY KEY,
    tags text[]
);

Массивы поддерживают операции срезов (tags[1:2]), конкатенацию (||), поиск вхождения (@>, <@) и GIN-индексы для быстрого поиска по элементам. Ограничение: массив — это значение одного столбца, поэтому ссылочная целостность на отдельные элементы не распространяется.

JSON и JSONB закрывают случай, когда структура документа заранее неизвестна или часто меняется. Разница принципиальна: json хранит исходный текст как есть и перепроверяет его при каждом обращении, jsonb разбирает документ при записи в бинарное представление, удаляя дубликаты ключей и не сохраняя порядок и пробелы. Для запросов почти всегда выбирают jsonb — по нему работают GIN-индексы (jsonb_path_ops), а операторы @>, ?, #> не требуют повторного парсинга.

SELECT * FROM events
WHERE payload @> '{"level": "error"}';

Диапазонные типы (int4range, tsrange, daterange) представляют интервал одним значением и умеют проверять пересечения оператором &&. Бронирование переговорной на два часа описывается одной строкой вместо двух полей «начало» и «конец» — и конфликт брони ловится одним условием, а не двумя сравнениями.

Ещё один слой объектности — наследование таблиц. Дочерняя таблица через INHERITS получает все столбцы родителя и добавляет свои. Запрос к родителю по умолчанию обходит потомков (SELECT ... FROM parent включает их, если не указать ONLY). Этот механизм удобен для секционирования вручную и для хранения разнородных сущностей с общей базой полей, хотя сегодня для секционирования чаще берут декларативный PARTITION BY.

Гибкость не бесплатна. Пользовательские типы и jsonb выходят за рамки простых скалярных столбцов, поэтому планировщик не всегда может оценить селективность так же точно, как для integer или text. Чем свободнее структура, тем внимательнее стоит относиться к индексам: GIN по jsonb ускоряет поиск, но увеличивает размер базы и замедляет запись.

Практический итог: реляционная модель в PostgreSQL не отменяется — она расширяется. Вы по-прежнему работаете с таблицами и транзакциями, но при необходимости описываете собственные типы, храните массивы и документы прямо в столбцах и получаете индексы под них. Это позволяет держать связанные данные в одной таблице и не разносить их по десятку вспомогательных, сохраняя при этом контроль на уровне схемы.

Расширяемость: как PostgreSQL превратился в платформу для инноваций

CREATE TYPE — это не только «строка внутри строки». Полноценный пользовательский тип в PostgreSQL включает текстовый ввод/вывод, бинарный формат и операции сравнения. Именно поэтому новый тип живёт в системе на равных правах со встроенными: планировщик умеет его сравнивать, сортировать и индексировать.

Практический шаг — зарегистрировать функции, которые описывают поведение типа на разных уровнях:

  • in и out — преобразование между внешним текстовым представлением и внутренней структурой;
  • send и recv — бинарная передача по протоколу;
  • операторы сравнения (=, <, >) и функция класса операторов.

Как только эти функции есть, поверх типа можно построить индекс. Postgres не заставляет вас ограничиваться B-tree: интерфейс CREATE OPERATOR CLASS описывает, как индексный метод (B-tree, GiST, GIN, SP-GiST, BRIN, Hash) взаимодействует с типом. Для GIN вы перечисляете поддерживаемые операторы и «extract value» функции, для GiST — набор стратегий и опорных функций. Ядро при этом не трогается: новый индексный метод или класс операторов подключается как отдельный модуль.

Функции расширяют эту схему дальше. Помимо SQL и PL/pgSQL, можно писать на C и регистрировать функцию с указанием языка, аргументов, возвращаемого типа и volatility (VOLATILE, STABLE, IMMUTABLE). Флаг IMMUTABLE разрешает планировщику сворачивать вызов на этапе подготовки запроса — например, подставлять результат для константного аргумента. Есть и агрегаты: CREATE AGGREGATE собирает новый агрегат из sfunc (шаг), stype (состояние), finalfunc (финализация) и, при необходимости, combinefunc для параллельного выполнения.

Всё это удобно упаковывается в extension. Файл .control описывает метаданные, а SQL-скрипты (install/upgrade) создают типы, операторы, индексы и функции. CREATE EXTENSION выполняет скрипт в целевой базе, и расширение становится обычным объектом, который можно обновлять или удалять. Так новая функциональность попадает в систему без пересборки сервера и без изменения его исходного кода — точки расширения заданы заранее и доступны любому модулю.

Сообщество и лицензия: почему открытость стала драйвером популярности

Лицензия PostgreSQL — не просто юридическая формальность, а практический инструмент, который влияет на то, как компании и разработчики строят свои продукты и сообщества вокруг базы данных.

Свобода без ограничений. PostgreSQL распространяется под лицензией PostgreSQL License — одной из самых либеральных среди открытых лицензий. Она позволяет использовать, модифицировать и распространять код без обязательства открывать собственные изменения. Это принципиально отличается от copyleft-лицензий, таких как GPL, где производные работы должны распространяться на тех же условиях. Для бизнеса это означает: можно встроить PostgreSQL в коммерческий продукт, модифицировать ядро под свои нужды и не публиковать свои доработки.

Практический пример. Компания создаёт SaaS-платформу и использует PostgreSQL как основу. Благодаря лицензии она может доработать движок под свои требования — например, добавить специфичную обработку запросов — и не обязана делиться этими изменениями с конкурентами. Это снижает юридические риски и ускоряет выход продукта на рынок.

Доверие через прозрачность. Либеральная лицензия работает в связке с открытым процессом разработки. Любой может изучить код, предложить патч или сообщить об уязвимости. Именно эта открытость формирует доверие: компании видят, что база данных не контролируется одной корпорацией, и могут влиять на её развитие через сообщество.

Активное сообщество как двигатель. Вокруг PostgreSQL сложилось одно из крупнейших сообществ в мире открытых баз данных. Регулярные конференции, mailing lists, комитеты по разработке — всё это обеспечивает быструю эволюцию продукта. Новые возможности появляются не потому, что так решил вендор, а потому что сообщество их запросило и реализовало.

Итог: лицензия и сообщество — две стороны одной медали. Первая даёт юридическую свободу, второе — техническую и социальную поддержку. Вместе они создают экосистему, в которую компании готовы вкладываться, не опасаясь, что завтра условия изменятся.

MVCC и целостность: надёжность под высокой нагрузкой

Что произойдёт, если один пользователь оформляет заказ, а другой в ту же секунду меняет остаток товара на складе? Без продуманного механизма кто-то увидит устаревшие данные или получит ошибку. В PostgreSQL эту задачу решают две вещи, работающие в связке: многоверсионность (MVCC) и строгие гарантии ACID.

MVCC: читатели не блокируют писателей. Вместо того чтобы перезаписывать строку на месте, PostgreSQL создаёт её новую версию. Старая версия остаётся видимой для транзакций, которые начались раньше. В результате читающий запрос не ждёт, пока завершится запись, а пишущий не блокирует чтение. Пример: аналитик строит отчёт по таблице заказов, пока в неё непрерывно пишутся новые строки, — отчёт опирается на согласованный снимок данных, а вставки идут своим ходом и не тормозят выборку.

ACID: четыре гарантии, которые держат данные в целости. Атомарность означает, что транзакция выполняется целиком или не выполняется вовсе. Если при переводе денег между счетами списание прошло, а зачисление упало, вся операция откатывается — половинчатого состояния не остаётся. Согласованность следит, чтобы транзакция переводила базу из одного корректного состояния в другое, не нарушая ограничений. Изоляция не даёт параллельным транзакциям видеть промежуточные результаты друг друга. Долговечность гарантирует: как только транзакция подтверждена, её изменения переживут сбой питания или падение процесса — они уже зафиксированы в журнале предзаписи (WAL).

Как это ощущается на практике. Вы задаёте нужный уровень изоляции под задачу. Для большинства сценариев хватает значения по умолчанию — Read Committed: каждая инструкция видит данные, зафиксированные до её начала. Там, где важна воспроизводимость отчёта, можно перейти на Repeatable Read и получить стабильный снимок на всю транзакцию. А для логики, чувствительной к гонкам, — например, проверки уникальности брони, — доступен Serializable, при котором конфликтующие транзакции завершатся ошибкой сериализации, и её можно осознанно повторить.

Так многоверсионность снимает лишние блокировки, а ACID добавляет предсказуемые гарантии поверх неё. Вместе они дают то, ради чего базу и выбирают: под нагрузкой данные остаются целыми, а параллельные пользователи не мешают друг другу.

Современные сценарии: от OLTP до аналитики и JSON

Возьмите интернет-магазин: касса принимает заказы, а аналитики тем же вечером считают выручку по категориям. Классический путь — держать две базы: одну для быстрых операций, вторую для отчётов, и постоянно переливать данные между ними. PostgreSQL предлагает другой вариант — закрывать оба сценария одним узлом.

Причина в расширяемости ядра. Поверх базового движка вы добавляете нужные возможности отдельными расширениями, не переписывая саму СУБД:

  • PostGIS превращает PostgreSQL в полноценную геобазу: хранит координаты, считает расстояния и пересечения прямо в SQL. Служба доставки может одним запросом найти курьеров в радиусе двух километров от адреса клиента.
  • pgvector хранит векторные представления рядом с обычными строками. Поиск похожих товаров по фото или ответы в стиле семантического поиска выполняются тем же соединением, что и транзакции.
  • TimescaleDB рассчитан на временные ряды: метрики серверов, показания датчиков, тики цен. Данные разбиваются на части по времени, и старые фрагменты читаются быстрее.

Расширения здесь не надстройка сбоку — они видны планировщику. Запрос может одновременно отфильтровать заказы по геозоне, посчитать их по дням и подтянуть векторное сходство, и всё это выполнится в одной транзакции. Отдельный сервис для аналитики тогда не нужен: отчётные запросы идут по тем же таблицам, где лежат актуальные данные.

Дополняет картину развитая система репликации и партиционирования. Тяжёлые аналитические запросы удобно уводить на реплику-читатель, разгружая основной узел, а крупные таблицы делить на партиции — тогда выборка по узкому диапазону затрагивает лишь часть данных. Так один PostgreSQL масштабируется под нагрузку, которая иначе потребовала бы зоопарка из нескольких систем.

Выводы: три ключевых фактора успеха и один практический совет

Три мысли, которые стоит унести из статьи.

Расширения важнее размера ядра. Выбор PostgreSQL — это не выбор одной СУБД, а выбор платформы, к которой подключаются модули под конкретную задачу. Геоданные, векторы, аналитика — всё ставится поверх базового движка, а не заменяет его.

Один узел справляется с разнородной нагрузкой. Транзакции и отчёты не обязаны жить в разных базах с постоянной синхронизацией. Если задачи укладываются в возможности расширений, отдельный аналитический кластер может вообще не понадобиться.

Открытость лицензии — это долгосрочная страховка. Отсутствие привязки к одному поставщику значит, что вы можете менять хостинг, команду и подрядчиков, не переписывая приложение.

Практический совет: прежде чем добавлять новую базу в архитектуру, проверьте, нет ли в экосистеме расширения под вашу задачу — часто дешевле подключить модуль к существующему PostgreSQL, чем поддерживать второй движок.

Похожее