Learning Platform
Урок 02.04 · 22 мин
Продвинутый
TablespacerelfilenodeSegmentsFree Space MapVisibility MapForksPGDATA

В предыдущих уроках мы говорили «таблица лежит в файле», но не объясняли — в каком именно. Этот урок закрывает дыру: где конкретно на диске находится каждая ваша таблица, как Postgres находит её среди сотен тысяч файлов, что лежит рядом с основным heap-файлом и зачем оно нужно.

Это база, без которой невозможно ни восстановление после катастрофы, ни оптимизация I/O между дисками, ни анализ bloat. Когда DBA говорит «таблица занимает 5 файлов по 1 GiB», или «free space map устарел», ты должен понимать, о чём речь, физически.

Что лежит в $PGDATA

$PGDATA — это директория данных кластера PostgreSQL (SHOW data_directory). Внутри неё структура:

$PGDATA/
  base/              ← все базы кластера
    1/               ← template1
    13412/           ← postgres (один из системных oid)
    16384/           ← твоя БД
      2615           ← системная таблица (pg_namespace)
      16400          ← одна из твоих таблиц
      16400_fsm      ← Free Space Map для неё
      16400_vm       ← Visibility Map для неё
      16400.1        ← второй сегмент heap, если таблица > 1 GiB
  global/            ← кластерные таблицы (pg_database и др.)
  pg_wal/            ← WAL-журналы
  pg_tblspc/         ← символические ссылки на пользовательские tablespaces

Каждая база — это директория с числовым именем (oid из pg_database). Внутри неё — файлы. И вот ключевой момент: имя файла — это relfilenode, а не имя таблицы и не её oid.

От имени таблицы к файлу на диске

Имя таблицы → pg_class.oid и pg_class.relfilenode → файл в base/<dbid>/<relfilenode>.

имя таблицыcustomers
pg_class.oid16400
pg_class.relfilenode16400 (вначале совпадает)
файл$PGDATA/base/16384/16400
после VACUUM FULLoid остаётся 16400, но relfilenode → 16732 (новый файл)

oid vs relfilenode: важное различие

При создании таблицы oid и relfilenode совпадают. Но операции, которые переписывают таблицуVACUUM FULL, CLUSTER, REINDEX, TRUNCATE, ALTER TABLE ... SET TABLESPACE — создают новый файл с новым relfilenode и атомарно переключают pg_class.relfilenode. Старый файл удаляется в конце транзакции.

Зачем такая сложность? Атомарность. Если бы Postgres переписывал файл «in-place», а сервер бы упал посередине — данные были бы повреждены. Через relfilenode-indirection всё чисто: либо коммит и новый файл валиден, либо rollback и старый цел.

Практическое следствие: имя файла на диске не стабильно. Если ты хочешь по pid дампа найти таблицу — спрашивай через pg_filenode_relation():

oid и relfilenode для customers и orders. До любых VACUUM FULL — они совпадают:

PostgreSQL

Демонстрация: после VACUUM FULL relfilenode меняется, oid — нет:

PostgreSQL

Заметь: oid не изменился (на него ссылаются foreign keys, права, статистика), а relfilenode стал другим. На диске старый файл удалён, новый создан.

Сегменты по 1 GiB

Один heap-файл не может быть больше 1 GiB. Когда таблица перерастает этот лимит, Postgres создаёт следующий файл — сегмент — с суффиксом .1, .2, и т.д.:

$PGDATA/base/16384/
  16400        ← сегмент 0 (первый 1 GiB)
  16400.1      ← сегмент 1 (следующий 1 GiB)
  16400.2      ← сегмент 2
  ...

Это историческое наследие: некоторые файловые системы (в первую очередь ранние версии ext, FAT32) плохо работали с файлами > 2 GiB. Сегодня все современные FS (ext4, xfs, zfs, apfs) спокойно держат гигабайтные файлы — но Postgres придерживается традиции из соображений совместимости и для удобства массового бэкапа. Логически это один heap; для клиента сегментация полностью прозрачна.

Размер сегмента задаётся на компиляции (--with-segsize, по умолчанию 1 GiB), как и BLCKSZ. В runtime поменять нельзя.

Forks: .fsm, .vm, init

Кроме основного heap-файла рядом с таблицей лежат forks — дополнительные файлы с тем же relfilenode, но суффиксами:

.fsm
Free Space Map. Это маленькая древовидная структура, которая для каждой страницы heap-файла хранит, сколько свободного места на ней осталось (с гранулярностью ~32 байта). Когда приходит INSERT, Postgres смотрит в FSM и за O(log N) находит страницу, на которой новый кортеж точно поместится. Без FSM пришлось бы либо линейно сканировать страницы, либо тупо аппендить в конец (и любой UPDATE с удалением создавал бы вечный bloat).

.vm
Visibility Map. Это компактная карта: для каждой страницы heap хранится 2 бита: all-visible (на странице нет dead tuples и нет апдейтов от незакоммиченных транзакций) и all-frozen (все tuples заморожены, transaction id уже не имеет значения). VM критична для двух оптимизаций: index-only scan (можно вернуть результат из индекса, не заходя в heap, если страница помечена all-visible) и incremental VACUUM (пропускать страницы all-visible — их и так чистить нечего).

init — пустой fork-маркер, существует только для unlogged-таблиц. Используется для re-init таблицы после crash recovery: вместо WAL-replay просто копируется initial state.

Файлы одной таблицы на диске

Основной heap, его сегменты, FSM и VM forks. Все имеют одинаковый relfilenode.

таблица customers (relfilenode 16400)всё в $PGDATA/base/16384/
16400heap, сегмент 0
16400.1heap, сегмент 1
16400_fsmFree Space Map
16400_vmVisibility Map

Размеры этих forks обычно невелики:

  • FSM: ~3 байта на heap-страницу (так ~3 MiB на 1 GiB heap).
  • VM: ~2 бита на heap-страницу (так ~32 KiB на 1 GiB heap).

Но они критически важны для производительности. Если FSM «отстал» от реальности (баги, sigkill, очень редко) — INSERT’ы начинают писать в конец, и таблица растёт. Если VM не обновлён — index-only scan вырождается в обычный index scan + heap fetch.

Размеры всех forks таблицы customers — heap, fsm, vm и индексы:

PostgreSQL

pg_relation_size(relation, fork) принимает второй аргумент — имя fork’а: 'main', 'fsm', 'vm', 'init'. По умолчанию (без аргумента) — main.

Tablespaces: положить таблицу на другой диск

Default tablespaces — pg_default (для пользовательских таблиц, файлы в $PGDATA/base/) и pg_global (для кластерных, в $PGDATA/global/). Но ты можешь создать свой tablespace, указывающий на другую директорию (например, на NVMe-диск отдельно от основного HDD):

CREATE TABLESPACE fast LOCATION '/mnt/nvme/pgdata';

CREATE TABLE hot_orders (...) TABLESPACE fast;
-- или перенести существующую:
ALTER TABLE old_orders SET TABLESPACE fast;

Физически tablespace — это символическая ссылка в $PGDATA/pg_tblspc/<tblspc_oid> на ту директорию, что ты указал. Внутри неё структура <pg_version>/<dbid>/<relfilenode> — такая же, как в base/.

Зачем это нужно:

  1. Разделение по дискам: горячие OLTP-таблицы на NVMe, архив на HDD.
  2. Разделение по объёму: главный диск близок к 100%, новые таблицы — на свежем большом массиве.
  3. Раздельный fs-tuning: на NVMe-диске с короткими fsync — журналы, на HDD с longer commit — холодный архив.
  4. Read-only mounts: tablespace на read-only диске даёт защиту от случайных DROP.

Когда не нужно:

  • Чтобы «сделать таблицу быстрее». Если диск тот же — никакого выигрыша нет.
  • Для бэкапа: tablespace требует отдельной обработки в pg_basebackup и часто ломает простые cp -r $PGDATA.

Какие tablespaces определены в текущей БД? (В pglite будет только pg_default.)

PostgreSQL

Обычно в проде ты увидишь 1-3 tablespaces, не больше. Множить их без причины — путь к operational debt.

Поиск файла на диске

Допустим, ты подключился к серверу, видишь $PGDATA/base/16384/16400 и хочешь понять, какая это таблица. Делается через pg_filenode_relation(tablespace_oid, filenode):

Преобразуем relfilenode обратно в имя таблицы:

PostgreSQL

reltablespace = 0 означает «default tablespace» (либо pg_default, либо pg_global для системных). На реальной БД при инцидентах с поломкой каталога это иногда единственный способ восстановить связь файла с таблицей.

Главные практические следствия

  1. Имя файла нестабильно. Не пиши скрипты, ссылающиеся на relfilenode по hard-coded числу — он поменяется после первого VACUUM FULL.
  2. VACUUM FULL — это всегда новый файл. Это значит downtime (ACCESS EXCLUSIVE lock), удвоение диска на время операции, и сброс всей буферной памяти для этой таблицы.
  3. FSM и VM критичны. Они обновляются автовакуумом. Если ты долго не вакуумишь — index-only scan перестаёт работать, и INSERT’ы начинают аппендить в конец, а не заполнять освободившиеся страницы.
  4. Tablespaces — для физического разделения дисков, а не для логической организации. Логику оставь schema’м.
Проверка знанийKnowledge check
Ты получил алерт: на дисковой партиции, где живёт $PGDATA, осталось 5% места. ALTER TABLE ... SET TABLESPACE fast перенесёт таблицу на другой диск. Что нужно учесть до запуска, и сколько места временно потребуется?
ОтветAnswer
SET TABLESPACE физически копирует данные на новый диск, создавая новый relfilenode с теми же данными, а старый файл удаляется в конце транзакции. На время операции потребуется столько же места, сколько таблица занимает СЕЙЧАС, плюс на старом диске. Если место уже 5%, а таблица — 60% диска, операция упадёт по space exhausted. Кроме того: (1) ACCESS EXCLUSIVE lock на всё время копирования — никто не сможет читать/писать таблицу; (2) WAL пишется как обычно — следи за pg_wal на исходном диске; (3) индексы НЕ переносятся автоматически, для них нужен отдельный ALTER INDEX ... SET TABLESPACE; (4) после успеха старый relfilenode удалится — но место на старом диске освободится только после COMMIT, не сразу. Альтернатива для критических ситуаций — pg_repack на тот же tablespace (не освободит проблемный диск) или внешнее переключение через replica.
VFS: единый интерфейс над ext4, xfs, btrfs, tmpfs fsync и durability: когда write на самом деле записан на диск

Чек-лист

  • $PGDATA/base/<dbid>/<relfilenode> — путь к heap-файлу таблицы. dbid берётся из pg_database.oid, relfilenode — из pg_class.relfilenode.
  • Сегменты по 1 GiB: <relfilenode>, <relfilenode>.1, <relfilenode>.2. Прозрачно для запросов.
  • oid стабилен, relfilenode — нет. После VACUUM FULL, CLUSTER, TRUNCATE, ALTER TABLE SET TABLESPACE — новый файл, новый relfilenode, тот же oid.
  • Forks: _fsm (Free Space Map — где есть место), _vm (Visibility Map — кто all-visible/all-frozen для index-only scan и быстрого VACUUM), _init (для unlogged).
  • Tablespace — способ положить таблицу на другой диск. CREATE TABLESPACE + ALTER TABLE ... SET TABLESPACE.
  • pg_filenode_relation(reltablespace, relfilenode) — функция, преобразующая имя файла обратно в имя таблицы.
  • pg_relation_size(rel, fork) поддерживает второй аргумент: 'main' / 'fsm' / 'vm' / 'init'.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём разница между pg_class.oid и pg_class.relfilenode?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5