Физическая структура хранения

Предпосылки: страницы и кортежи, TOAST.

TOAST | WAL

Страницы и кортежи описывают, как данные устроены внутри файла. 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


TOAST | WAL