1 Введение
PostgreSQL 18 · Издание 2026 года · Китайский перевод доступен для PostgreSQL 18
1.1. Организация данных
Базы данных
PostgreSQL — программа, которая относится к классу систем управления базами данных. Когда эта программа выполняется, мы называем ее сервером PostgreSQL, или экземпляром сервера.
Данные, которыми управляет PostgreSQL, хранятся в базах данных 1. Один экземпляр PostgreSQL одновременно работает с несколькими базами, которые вместе называются кластером баз данных.
Чтобы кластер можно было использовать, его необходимо инициализировать 2 (создать). Каталог, в котором размещаются все файлы, относящиеся к кластеру, обычно называют словом PGDATA, по имени переменной окружения, указывающей на этот каталог.
Пакетные установки могут вводить свои «слои абстракции» над штатным механизмом PostgreSQL, явно прописывая все необходимые утилитам параметры. Сервер баз данных представляется в таком случае в виде службы операционной системы, и непосредственно с переменной PGDATA можно никогда не встретиться. Но сам термин устоялся, и я буду им пользоваться.
При инициализации в PGDATA создаются три одинаковые базы данных:
template0 используется, например, для восстановления из логической резервной копии или для создания базы в другой кодировке и никогда не должна меняться; (с. 631)
template1 служит шаблоном для всех остальных баз данных, которые может создать пользователь в этом кластере;
postgres представляет собой обычную базу данных, которую можно использовать по своему усмотрению.
Системный каталог
Метаинформация обо всех объектах кластера (таких как таблицы, индексы, типы данных или функции) хранится в таблицах, относящихся к системному каталогу 3. В каждой базе данных имеется собственный набор таблиц (и представлений), описывающих объекты этой конкретной базы. Существует также несколько таблиц системного каталога, общих для всего кластера, которые не принадлежат какой-либо определенной базе данных (формально используется фиктивная база с нулевым идентификатором), но доступны в любой из них.
К системному каталогу можно обращаться с помощью обычных запросов SQL, а изменения в нем происходят при выполнении команд DDL. Клиент psql располагает целым рядом специальных команд для просмотра системного каталога.
Все имена таблиц системного каталога имеют префикс pg_, например pg_database. Имена столбцов начинаются с трехбуквенного кода, который обычно соответствует имени таблицы, например datname.
Во всех таблицах системного каталога столбец с первичным ключом называется oid и имеет одноименный тип oid (object identifier) — целое 32-битное число.
По сути, механизм идентификаторов объектов oid — аналог последовательностей, появившийся в PostgreSQL задолго до самих последовательностей. Его особенность в том, что уникальные номера используются в разных таблицах системного каталога, хотя выдаются с помощью единого счетчика. Когда общее число выданных номеров выходит за диапазон значений, счетчик обнуляется. Уникальность значений в конкретной таблице гарантируется тем, что очередное значение oid проверяется с помощью уникального индекса и, если оно уже используется в этой таблице, счетчик увеличивается, а проверка повторяется4.
Схемы
Схемы 5 представляют собой пространства имен для всех объектов, хранящихся в базе данных. Кроме пользовательских схем, имеется несколько специальных служебных:
public используется по умолчанию для пользовательских объектов, если не выполнены иные настройки;
pg_catalog используется для таблиц системного каталога;
information_schema дает альтернативное представление системного каталога, регламентируемое стандартом SQL;
pg_toast используется для объектов, относящихся к TOAST; (с. 33)
pg_temp объединяет временные таблицы (хотя временные таблицы разных пользователей создаются в разных схемах pg_temp_N, каждый обращается к своим объектам, используя имя pg_temp).
Схемы существуют внутри базы данных, и все объекты базы принадлежат каким-либо схемам.
Если при обращении к объекту схема не указана явно, выбирается первая подходящая схема из перечисленных в пути поиска. Путь формируется на основе значения параметра search_path, к которому, в частности, неявно добавляются схемы pg_catalog и (при необходимости) pg_temp. Это позволяет иметь в разных схемах объекты с одинаковыми именами.
Табличные пространства
В отличие от логического распределения объектов по базам данных и схемам, табличные пространства определяют физическое расположение данных. Фактически табличное пространство — это каталог файловой системы. Например, табличные пространства можно использовать, чтобы разместить архивные данные на медленных носителях, а данные, с которыми идет активная работа, — на быстрых.
Одно и то же табличное пространство может использоваться разными базами данных, а одна база данных может хранить данные в нескольких табличных пространствах. То есть логическая и физическая структуры не зависят друг от друга.
У каждой базы данных есть так называемое табличное пространство по умолчанию, в котором создаются все объекты, если явно не указать иное. В этом же табличном пространстве хранятся и объекты системного каталога.
При инициализации кластера создаются два табличных пространства:
pg_default располагается в каталоге PGDATA/base и используется как табличное пространство по умолчанию, если явно не выбрать для этой цели другое пространство;
pg_global располагается в каталоге PGDATA/global и хранит общие для всего кластера объекты системного каталога.
При создании пользовательского табличного пространства указывается произвольный каталог; PostgreSQL создает на него символьную ссылку в каталоге PGDATA/pg_tblspc. Вообще, все пути PostgreSQL относительные и отсчитываются от каталога данных PGDATA. Благодаря этому можно переносить PGDATA на другое место (конечно, предварительно остановив сервер).
Рисунок сводит воедино базы данных, схемы и табличные пространства. Здесь база данных postgres использует табличное пространство по умолчанию xyzzy, а база template1 — pg_default. На пересечении табличных пространств и схем находятся различные объекты базы данных:
Отношения
Самые важные объекты базы данных — таблицы и индексы — при всех своих различиях схожи в том, что состоят из строк. Для таблиц это очевидно, но и в B-деревьях узлы состоят из строк, содержащих индексированные значения и ссылки на другие узлы или на табличные строки. (с. 523)
Есть еще некоторое количество объектов, устроенных похожим образом: последовательности (по сути однострочные таблицы), материализованные представления (по сути таблицы, которые помнят запрос). А еще есть обычные представления, которые сами по себе не хранят данные, но во всех остальных смыслах похожи на таблицы.
Все эти объекты в PostgreSQL называются общим словом отношение (relation).
Слово, на мой взгляд, выбрано неудачно, поскольку смешивает таблицы базы данных и (настоящие) отношения реляционной теории. Дает о себе знать университетское происхождение проекта и склонность его основателя, Майкла Стоунбрейкера, во всем видеть отношения. В одной из работ он даже ввел понятие «упорядоченного отношения» (ordered relation) для таблицы, в которой порядок строк задается индексом.
Таблица системного каталога для отношений изначально называлась pg_relation, но довольно скоро, на волне увлечения объектной ориентированностью, ее переименовали в привычный нам сейчас pg_class. Однако столбцы этой таблицы по-прежнему имеют префикс rel.
Слои и файлы
Информация, связанная с отношением, организована в несколько слоев 6 (forks) разных типов, каждый из них содержит определенный вид данных.
Изначально слой представлен единственным файлом. Имя файла состоит из числового идентификатора (oid), к которому может быть добавлен суффикс, соответствующий типу слоя.
Файл постепенно растет, и когда его размер достигает 1 Гбайта, создается следующий файл этого же слоя (такие файлы иногда называют сегментами). Порядковый номер сегмента добавляется в конец имени файла.
Ограничение размера файла в 1 Гбайт возникло исторически для поддержки различных файловых систем, некоторые из которых не умеют работать с файлами большого размера. Ограничение можно изменить при сборке PostgreSQL (./configure --with-segsize).
Таким образом, одному отношению на диске может соответствовать несколько файлов. Даже для небольшой таблицы без индексов их будет минимум три, по числу обязательных слоев.
Внутри каталога каждого табличного пространства (кроме pg_global) создаются подкаталоги для отдельных баз данных. Все файлы объектов, принадлежащих одному табличному пространству и одной базе данных, будут помещены в один подкаталог. Это необходимо учитывать, потому что файловые системы обычно не очень хорошо работают с большим количеством файлов в каталоге.
Имеется несколько стандартных типов слоев.
Основной слой (main fork) — это собственно данные: те самые табличные или индексные строки. Основной слой существует для любых отношений (кроме представлений, которые не содержат данных).
Имена файлов основного слоя состоят только из числового идентификатора (равного значению столбца relfilenode таблицы pg_class).
Посмотрим для примера путь к файлу таблицы, созданной в табличном пространстве pg_default:
Каталог base соответствует табличному пространству pg_default, следующий подкаталог — базе данных, и уже в нем находится интересующий нас файл:
Вот соответствующий файл в файловой системе:
Слой инициализации (init fork) 7 существует только для нежурналируемых таблиц (созданных с указанием UNLOGGED) и их индексов. Такие объекты ничем не отличаются от обычных, кроме того, что действия с ними не записываются в журнал предзаписи. За счет этого работа с ними происходит быстрее, но в случае сбоя невозможно восстановить данные в согласованном состоянии. Поэтому при восстановлении PostgreSQL просто удаляет все слои таких объектов и записывает слой инициализации на место основного слоя. В результате получается «пустышка». (с. 211)
Таблица t создана нежурналируемой, поэтому у нее есть слой инициализации. Он имеет такое же имя, как и основной слой, но с суффиксом _init:
Карта свободного пространства (free space map) 8 — слой, в котором отслеживается примерный объем свободного места внутри страниц. Этот объем постоянно меняется: при добавлении новых версий строк уменьшается, при очистке — увеличивается. Карта свободного пространства используется при вставке новых версий строк, чтобы быстро найти подходящую страницу, на которую поместятся добавляемые данные.
Файлы, относящиеся к карте свободного пространства, имеют суффикс _fsm. Но появляются они не сразу, а только при необходимости. Самый простой способ добиться этого — выполнить очистку таблицы: (с. 133)
Для ускорения поиска карта свободного пространства организована как дерево и занимает минимум три страницы (отсюда и размер файла слоя для почти пустой таблицы).
Карта свободного пространства существует не только для таблиц, но и для индексов. Но поскольку индексную строку нельзя добавить на произвольную страницу (например, для B-дерева место вставки определяется порядком сортировки), отслеживаются только те страницы, которые были полностью очищены и могут быть снова задействованы в индексной структуре.
Карта видимости (visibility map) 9 — слой, который позволяет быстро определить, требует ли страница очистки или заморозки. Для этого на каждую табличную страницу в этом слое отведено два бита.
Одним битом отмечены страницы, которые содержат только актуальные версии строк. Процесс очистки пропускает такие страницы, поскольку в них нечего очищать. Кроме того, когда транзакция пытается прочитать строку из такой страницы, можно не проверять ее видимость, а это позволяет использовать сканирование только индекса. (с. 131) (с. 422)
Второй бит отмечает страницы, на которых все версии строк заморожены. Эту часть слоя я буду называть картой заморозки. (с. 134)
Файлы карты видимости имеют суффикс _vm. Обычно они самые небольшие по размеру:
Карта видимости существует для таблиц, но не для индексов. (с. 89)
Страницы
Для удобства организации ввода-вывода файлы логически поделены на страницы (или блоки) — это минимальный объем данных, который считывается или записывается. Соответственно, и многие внутренние алгоритмы PostgreSQL ориентированы на работу со страницами. (с. 75)
Обычно страница имеет размер 8 Кбайт. Его можно поменять в некоторых пределах (до 32 Кбайт), но только при сборке (./configure --with-blocksize), и, как правило, никто этим не занимается. Собранный и запущенный экземпляр может работать со страницами только одного размера — создать табличные пространства с разноразмерными страницами нельзя.
Независимо от того, к какому слою принадлежат файлы, они используются сервером примерно одинаково. Страницы сначала помещаются в буферный кеш (где их могут читать и изменять процессы), а затем при необходимости вытесняются обратно на диск. (с. 183)
TOAST
Каждая строка должна помещаться целиком на одну страницу: нет способа «продолжить» строку на следующей странице. Для длинных строк используется технология, названная TOAST 10 (The Oversized Attributes Storage Technique).
TOAST подразумевает несколько стратегий. Длинные значения атрибутов можно отправить в отдельную служебную таблицу, предварительно нарезав на небольшие фрагменты-«тосты». Другой вариант — сжать длинное значение так, чтобы строка все-таки поместилась на одну страницу. А можно и то и другое: сначала сжать, а уже потом нарезать и отправить.
Если основная таблица содержит потенциально длинные атрибуты, для нее сразу же создается отдельная toast-таблица (одна для всех атрибутов). Например, если в таблице есть столбец типа numeric или text, toast-таблица будет создана, даже если в таком столбце никогда не будут храниться длинные значения.
Для индексов технология TOAST предлагает только сжатие; вынесение атрибутов в отдельную таблицу не поддерживается. Это накладывает ограничение на размер индексируемых ключей (как это ограничение реализуется, зависит от класса операторов). (с. 397)
Обычно стратегии определяются типами столбцов. Посмотреть стратегии проще всего командой \d+ в psql, но для сокращения вывода я использую запрос к системному каталогу: (psql: \d+)
Стратегии состоят в следующем:
plain — TOAST не используется (стратегия применяется для заведомо «коротких» типов данных, как integer);
extended — допускается как сжатие, так и хранение в отдельной toast-таблице;
external — длинные значения хранятся в toast-таблице несжатыми;
main — длинные значения в первую очередь сжимаются, а в toast-таблицу попадают, только если сжатие не помогло.
В общих чертах алгоритм выглядит следующим образом11. PostgreSQL стремится к тому, чтобы на странице помещалось хотя бы четыре строки. Поэтому, если размер строки превышает четвертую часть страницы за вычетом заголовка (для страницы стандартного размера это около 2000 байт), к части значений необходимо применить TOAST. Действуем в порядке, описанном ниже, и прекращаем, как только длина строки перестает превышать пороговое значение.
Сначала перебираем атрибуты со стратегиями external и extended, двигаясь от самых длинных к более коротким. Extended-атрибуты сжимаются, и если после этого значение (само по себе, без учета других атрибутов) превосходит четверть страницы, оно сразу же отправляется в toast-таблицу. External-атрибуты обрабатываются так же, но не сжимаются.
Если после первого прохода строка все еще не помещается, по одному отправляем в toast-таблицу оставшиеся атрибуты со стратегиями external и extended.
Если и это не помогло, пытаемся сжать атрибуты со стратегией main, оставляя их при этом в табличной странице.
Если строка все равно недостаточно коротка, отправляем в toast-таблицу main-атрибуты.
Пороговое значение составляет все те же 2000 байт, но может переопределяться параметром хранения toast_tuple_target на уровне таблицы.
Иногда может оказаться полезным изменить стратегию для некоторых столбцов. Если заранее известно, что данные в столбце не сжимаются (например, в столбце хранятся JPEG-изображения), можно установить для него стратегию external — это позволит сэкономить на бесполезных попытках сжатия. Изменить стратегию можно как при создании таблицы (предложение STORAGE команды CREATE TABLE), так и впоследствии: (v. 16)
Повторив запрос, получим:
Для сжатия доступны два алгоритма: традиционный PGLZ и более современный LZ4 (оба являются разновидностями алгоритма словарного сжатия Лемпеля—Зива). По умолчанию сжатие выполняется алгоритмом, указанным в параметре default_toast_compression; значение можно установить для отдельных столбцов в предложении COMPRESSION. Эталонные тесты показывают, что при сходном с PGLZ уровне сжатия LZ4 тратит меньше ресурсов процессора. (v. 15) (по умолчанию: pglz)
Toast-таблицы располагаются в отдельной схеме pg_toast, не входящей в путь поиска, и поэтому обычно не видны. Для временных таблиц используется схема pg_toast_temp_N аналогично обычной pg_temp_N.
Конечно, при желании всегда можно подглядеть за внутренней механикой процесса. Скажем, в таблице t есть три потенциально длинных атрибута, поэтому toast-таблица обязана быть. Вот она:
Логично, что для «тостов», на которые нарезается строка, применяется стратегия plain: TOAST второго уровня не существует.
Вместе с toast-таблицей в той же схеме создается и индекс, который всегда используется для доступа к фрагментам значений. Имя индекса видно в выводе команды, но его можно найти и запросом:
Таким образом, toast-таблица увеличивает минимальное число файлов, «обслуживающих» таблицу, до восьми: три слоя основной таблицы, три слоя toast-таблицы и два слоя toast-индекса.
Столбец c использует стратегию extended, значения в нем будут сжиматься:
В toast-таблице ничего нет: повторяющиеся символы сжались алгоритмом PGLZ, и после этого значение поместилось в обычной табличной странице.
А теперь составим значение из случайных символов:
Такую последовательность сжать не получается. В табличную строку записывается идентификатор chunk_id:
А само значение попадает в toast-таблицу:
Видно, что данные нарезаны на фрагменты. Размер фрагментов выбирается так, чтобы на странице toast-таблицы помещалось четыре строки. От версии к версии это значение может немного меняться в зависимости от размера заголовка страницы.
При обращении к «длинному» атрибуту PostgreSQL автоматически, прозрачно для приложения, восстанавливает исходное значение и возвращает его клиенту. Если же такие атрибуты не участвуют в запросе, то toast-таблица не читается. Это одна из причин не использовать «звездочку» в производственном коде.
Если клиент запрашивает начальную часть длинного значения, то будут прочитаны только необходимые фрагменты, в том числе и в случае, когда значение хранится в сжатом виде. (v. 13)
Тем не менее и на сжатие с нарезкой, и на последующее восстановление тратится довольно много ресурсов. Поэтому хранить объемные данные в PostgreSQL — не лучшая идея, особенно если они активно используются и при этом для них не требуется транзакционная логика (как пример: отсканированные оригиналы бухгалтерских документов). Потенциально более выгодная альтернатива — хранить такие данные в файловой системе, а в базе данных держать только имена соответствующих файлов. Правда, тогда СУБД не сможет обеспечивать согласованность данных. (v. 17)
Затраты на распаковку значений, обработанных TOAST, позволяет оценить команда
EXPLAIN с параметрами analyze и serialize.
1.2. Процессы и память
Экземпляр сервера PostgreSQL состоит из нескольких взаимодействующих процессов.
В первую очередь при старте сервера запускается процесс postgres, традиционно называемый postmaster. Postmaster запускает все остальные процессы (в Unix-подобных системах для этого используется системный вызов fork) и «присматривает» за ними — если какой-нибудь процесс завершится аварийно, postmaster перезапустит его (или весь сервер, если сочтет, что процесс мог повредить общие данные).
Процессная модель применяется в PostgreSQL с самого начала проекта из-за своей простоты, и все это время не прекращаются дискуссии о переходе к использованию потоков.
У текущей модели есть ряд недостатков: статически выделяемая общая память не позволяет на лету менять размер таких структур, как буферный кеш; параллельные алгоритмы сложны в реализации и менее эффективны, чем могли бы быть; сеансы жестко привязаны к процессам. Использование потоков выглядит многообещающе, хотя и чревато сложностями с изолированностью, совместимостью c операционными системами, управлением ресурсами. Не говоря уже о том, что переход потребует радикальных изменений в коде и годы работы. Пока побеждает консервативный взгляд, и в ближайшее время никаких изменений не предвидится.
Работу сервера обеспечивает ряд служебных процессов. Основные из них:
startup восстанавливает систему после сбоев; (с. 223)
autovacuum очищает таблицы и индексы от неактуальных данных; (с. 138)
wal writer записывает на диск журнальные записи; (с. 236)
checkpointer выполняет контрольную точку; (с. 219)
writer записывает грязные страницы на диск; (с. 226)
wal sender передает журнальные записи на реплику;
wal receiver принимает журнальные записи на реплике.
Некоторые из этих процессов завершаются после выполнения своей задачи, другие работают постоянно в фоновом режиме, а какие-то могут быть отключены.
Каждый процесс управляется конфигурационными параметрами, иногда десятками параметров. Чтобы осмысленно выполнять настройку сервера, нужно хорошо представлять его внутреннее устройство. Но из общих соображений можно выбрать лишь начальные адекватные значения параметров, которые затем потребуется уточнять, получая обратную связь от мониторинга.
Чтобы процессы могли обмениваться информацией, postmaster выделяет общую память, которая доступна всем процессам.
Из-за того, что диски (особенно HDD, но и SSD тоже) работают значительно медленнее, чем оперативная память, применяется кеширование: в общей области оперативной памяти отводится место под недавно прочитанные страницы в надежде, что они еще не раз понадобятся и можно будет сэкономить на повторном обращении к диску. Измененные данные также записываются на диск не сразу, а через некоторое время. (с. 183)
Буферный кеш занимает бóльшую часть общей памяти. В ней же располагаются и другие буферы, которые используются сервером для ускорения работы с диском.
Свой кеш имеется и у операционной системы. PostgreSQL не использует (почти) прямой ввод-вывод в обход механизмов операционной системы, поэтому кеширование получается двойным.
При сбое (например, при аварийном отключении питания или крахе операционной системы) содержимое оперативной памяти, включая буферный кеш, пропадает. На диске остаются файлы, страницы которых записаны
в разные моменты времени. Чтобы иметь возможность восстановить согласованность данных, в процессе работы PostgreSQL ведет журнал предзаписи (WAL), позволяющий при необходимости выполнить потерянные операции повторно. (с. 211)
1.3. Клиенты и клиент-серверный протокол
Еще одна задача процесса postmaster — слушать входящие соединения. При появлении нового клиента postmaster порождает для него обслуживающий процесс 12 (backend). Клиент устанавливает соединение и начинает сеанс общения со своим серверным процессом. Сеанс продолжается до отключения клиента или разрыва связи.
Для каждого клиента на сервере порождается собственный обслуживающий процесс. Большое количество подключений может вызывать проблемы:
Каждому процессу требуется оперативная память для хранения кеша системного каталога, подготовленных запросов, промежуточных результатов при выполнении запросов и других данных. Чем больше соединений открыто, тем больше должно быть доступной памяти. (с. 340) (с. 338)
Если соединения выполняются часто, а сеансы при этом короткие (то есть клиент выполняет один небольшой запрос и отключается), непозволительно много ресурсов будет тратиться на установление соединения, порождение нового процесса и ненужное заполнение локальных кешей.
Чем больше запущено процессов, тем больше времени требуется на просмотр их списка, а эта операция выполняется очень часто. В результате с увеличением числа клиентов производительность может падать. (с. 99)
В таких случаях используют пул соединений, чтобы ограничить число обслуживающих процессов. PostgreSQL не имеет встроенного пула соединений, поэтому приходится применять сторонние решения: менеджеры пулов, встроенные в сервер приложений, или внешние программы (такие как PgBouncer 13 или Odyssey 14). При этом, как правило, один процесс на сервере поочередно выполняет транзакции разных клиентов. Это накладывает определенные ограничения на разработку приложений, позволяя использовать средства, которые локализованы в пределах транзакции, но не сеанса.
Чтобы клиент и сервер понимали друг друга, они должны использовать один и тот же протокол взаимодействия 15. Обычно для реализации протокола используют штатную библиотеку libpq, хотя встречаются и независимые реализации.
Говоря в самых общих чертах, протокол позволяет клиенту подключиться к серверу и выполнять SQL-запросы.
Подключение всегда выполняется под определенной ролью (пользователем) и к конкретной базе данных. Несмотря на то что сервер работает с кластером баз данных, для использования в приложении сразу нескольких баз придется выполнить отдельное подключение к каждой из них. При подключении выполняется аутентификация: обслуживающий процесс удостоверяется, что пользователь является тем, за кого себя выдает (например, спросив пароль), и проверяет полномочия пользователя на подключение к серверу и к выбранной базе данных.
SQL-запросы передаются обслуживающему процессу в текстовом виде. Процесс разбирает текст, оптимизирует запрос, выполняет его и возвращает результат клиенту.
backend/catalog/catalog.c, функция GetNewOidWithIndex. ↩︎
postgrespro.ru/docs/postgresql/18/storage-fsm; backend/storage/freespace/README. ↩︎
postgrespro.ru/docs/postgresql/18/storage-toast; include/access/heaptoast.h. ↩︎
backend/tcop/postgres.c, функция PostgresMain. ↩︎