Физическая структура хранения
Предпосылки: страницы и кортежи, TOAST.
Страницы и кортежи описывают, как данные устроены внутри файла. TOAST показал, что большие значения выносятся в отдельные файлы. Но где все эти файлы находятся на диске, почему их имена — числа, а не имена таблиц, и почему у одной таблицы может быть пять-шесть файлов? Эти вопросы возникают, как только нужно разобраться с bloat, аномальным I/O или восстановлением после сбоя.
Кластер, базы данных и объекты
Допустим, мониторинг показывает: директория PostgreSQL заняла 80 ГБ при ожидаемых 30. Нужно найти, что именно выросло. Для этого нужно понимать иерархию файлов.
Кластер — вся инсталляция PostgreSQL, управляемая одним сервером (термин исторический, не «кластер серверов»). Корневая директория кластера (PGDATA, обычно /var/lib/postgresql/data) содержит поддиректории для каждой базы данных, WAL-файлы и конфигурацию.
PGDATA/
├── base/ ← пользовательские базы данных
│ ├── 1/ ← template1 (OID = 1)
│ ├── 16384/ ← база myapp (OID = 16384)
│ │ ├── 16385 ← таблица users (main fork)
│ │ ├── 16385_fsm ← FSM для users
│ │ ├── 16385_vm ← VM для users
│ │ ├── 16390 ← таблица orders
│ │ ├── 16395 ← индекс users_pkey
│ │ └── ...
│ └── 16500/ ← другая база
├── global/ ← общие для кластера таблицы (pg_database, pg_authid)
├── pg_wal/ ← WAL-файлы
├── pg_xact/ ← CLOG (статусы транзакций, см. MVCC)
└── postgresql.conf, pg_hba.conf
Каждый объект (таблица, индекс, последовательность) идентифицируется OID (Object Identifier) — уникальным числом в системных каталогах. Имя файла на диске — не OID напрямую, а relfilenode: обычно совпадает с OID, но может измениться после операций, переписывающих объект (VACUUM FULL, CLUSTER, REINDEX, TRUNCATE).
Форки: несколько файлов одного объекта
Вернёмся к диагностике. В base/16384/ видим файлы 16385, 16385_fsm, 16385_vm — это три форка одной таблицы. Main fork содержит данные (страницы с кортежами). FSM fork (Free Space Map) — карта свободного места на каждой странице: при INSERT PostgreSQL смотрит сюда, чтобы найти страницу с достаточным свободным местом без перебора всех страниц. VM fork (Visibility Map) — битовая карта: помечена ли страница как all-visible (все кортежи на ней видимы всем транзакциям). VM используется VACUUM (пропускает all-visible страницы) и Index Only Scan (если страница all-visible, не нужно ходить в heap для проверки видимости; см. B-tree).
Индексы — отдельные файлы со своими страницами и своей FSM. Файл индекса не внутри файла таблицы.
Сегменты: ограничение в 1 ГБ
Один файл форка ограничен 1 ГБ (131 072 страницы по 8 КБ). Таблица крупнее 1 ГБ разбивается на сегменты:
16390 ← первый сегмент (страницы 0–131071)
16390.1 ← второй сегмент (страницы 131072–262143)
16390.2 ← третий сегмент
Если таблица orders занимает 5 ГБ, у неё 5 сегментов main fork плюс FSM и VM.
Как найти файл таблицы
-- pg_class — системный каталог с метаданными объектов
SELECT oid, relname, relfilenode, pg_relation_filepath('orders') AS path
FROM pg_class WHERE relname = 'orders';
-- oid | relname | relfilenode | path
-- -------+---------+-------------+-------------------
-- 16390 | orders | 16390 | base/16384/16390Размер на диске: pg_relation_size('orders', 'main') для main fork, pg_total_relation_size('orders') для всех форков плюс индексы плюс TOAST. Разница между pg_relation_size и pg_total_relation_size показывает, сколько занимают индексы и TOAST — полезно при диагностике bloat.
Связь компонентов
┌─────────────────────────────────────────────────────────────────┐
│ Файл таблицы (main fork) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Page 0 │ │ Page 1 │ │ Page 2 │ │ Page 3 │ ... │
│ │ tuples │ │ tuples │ │ tuples │ │ tuples │ │
│ │ctid=(0,*)│ │ctid=(1,*)│ │ctid=(2,*)│ │ctid=(3,*)│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
| | | |
v v v v
┌─────────────────────────────────────────────────────────────────┐
│ FSM fork │
│ Page 0: ~500 байт │ Page 1: 0 │ Page 2: ~4000 │ Page 3: ~200 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ VM fork │
│ Page 0: 1 (all-visible) │ Page 1: 0 │ Page 2: 1 │ Page 3: 0 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Файл индекса (отдельный файл!) │
│ B-tree страницы с записями: (ключ) -> (ctid) │
│ Например: 'Alice' -> (2, 5) = страница 2, позиция 5 │
└─────────────────────────────────────────────────────────────────┘WAL живёт в pg_wal/, CLOG (статусы транзакций для MVCC) — в pg_xact/. При аварийном восстановлении PostgreSQL читает WAL и применяет изменения к страницам в main fork, используя pd_lsn каждой страницы для определения, какие WAL-записи уже применены.
Sources
- PostgreSQL Documentation (пример: v16): Database File Layout, System Catalogs (
pg_class),pg_relation_filepath. https://www.postgresql.org/docs/16/storage-file-layout.html