4.1 Таблица - это несколько файлов
Таблица в PostgreSQL хранится не одним файлом, а набором параллельных файлов - форков (forks). Главный форк держит строки, остальные - служебные карты, которые ускоряют работу. Все они лежат рядом в подкаталоге базы и относятся к одной таблице. Полный разбор - в relfilenode-forks.
Зачем разносить по файлам? Чтобы служебные карты можно было читать и обновлять, не трогая основной массив данных. Карта свободного места крошечная по сравнению с таблицей, и держать её отдельным маленьким файлом дешевле, чем вплетать в данные.
4.2 Найти файл таблицы
Путь к главному файлу таблицы возвращает pg_relation_filepath:
SELECT pg_relation_filepath('flights');-- base/16384/16503
Читается так: base/ - каталог обычных баз, 16384 - OID базы
(совпадает со строкой lab из pg_database), 16503 - имя файла
таблицы. Это имя называют relfilenode.
Числа на твоей установке будут другими: идентификаторы выдаются по мере создания объектов. Важна структура пути, а не конкретные цифры.
4.3 Четыре форка
У отношения может быть до четырёх форков. Различают их по суффиксу имени файла:
| Форк | Суффикс | Что хранит |
|---|---|---|
| main | нет | сами строки таблицы (или записи индекса) |
| fsm | _fsm | карта свободного места по страницам |
| vm | _vm | карта видимости и заморозки |
| init | _init | пустой шаблон для нежурналируемых таблиц |
Главный форк есть всегда. Карта свободного места (_fsm) помогает
вставке быстро находить страницу с местом - подробно в
free-space-map. Карта видимости (_vm) отмечает страницы, где всё
видимо всем, и ускоряет чтение и очистку - о ней visibility-map.
Форк _init нужен только нежурналируемым (UNLOGGED) таблицам как
эталон для сброса после сбоя.
4.4 Форки появляются не сразу
Сразу после CREATE TABLE на диске только главный форк. Служебные
карты создаются позже - когда в них появляется смысл. Карту видимости,
например, заполняет VACUUM. Это легко увидеть на размере форков:
CREATE TABLE forklab (id int, note text);
INSERT INTO forklab SELECT g, 'row'||g FROM generate_series(1, 300) g;
SELECT pg_relation_size('forklab', 'vm') AS vm_before; -- 0: карты видимости ещё нетVACUUM forklab;
SELECT pg_relation_size('forklab', 'vm') AS vm_after; -- 8192: появился форк _vmДо VACUUM форк _vm имеет нулевой размер - его попросту нет. После -
это 8192 байта: одна страница карты на маленькую таблицу. Так
проявляется ленивость: PostgreSQL не заводит структуру, пока она не
понадобилась.
4.5 relfilenode - это не OID
У отношения два числа: OID (внутренний идентификатор в системном каталоге) и relfilenode (имя файла на диске). Поначалу они равны. Но команды, переписывающие таблицу целиком, создают новый файл с новым relfilenode, а OID оставляют прежним.
SELECT oid, relfilenode, oid = relfilenode AS same
FROM pg_class WHERE relname = 'forklab';
-- oid=16583, relfilenode=16583, same=t
VACUUM FULL forklab; -- переписывает таблицу в новый файл
SELECT oid, relfilenode, oid = relfilenode AS same
FROM pg_class WHERE relname = 'forklab';
-- oid=16583, relfilenode=16588, same=f
После VACUUM FULL OID тот же, а relfilenode сменился. То же делают
TRUNCATE, CLUSTER и ALTER TABLE, меняющий тип колонки. Поэтому в
коде, который должен пережить перезапись таблицы, ссылаются на OID, а
pg_relation_filepath всегда показывает актуальный relfilenode.
4.6 Сегменты по гигабайту
Один форк не растёт одним бесконечным файлом. Как только главный форк
переваливает за 1 ГБ, PostgreSQL продолжает в файле relfilenode.1,
потом relfilenode.2 и так далее. Это страховка от старых файловых
систем с лимитом на размер файла и удобство для операций копирования.
На практике это значит: большая таблица на диске - это пачка файлов
16503, 16503.1, 16503.2, а не один гигантский. Логически - одна
таблица, физически - набор сегментов по гигабайту.
4.7 Подводный камень: имя файла - не адрес навсегда
Соблазнительно записать relfilenode таблицы и потом обращаться к файлу
напрямую - например, в скрипте мониторинга. Не делай так. Любая
перезапись таблицы (VACUUM FULL, TRUNCATE, смена типа колонки) даст
новый relfilenode, и твоя ссылка станет указывать на удалённый или
чужой файл.
Стабилен только OID. Если нужен путь, спрашивай его заново через
pg_relation_filepath - он переведёт OID в актуальное имя файла. Имя
файла - это снимок на момент запроса, а не вечный адрес.
Уроки в sandbox
lab-4.1. Найди файл таблицы на диске
Пройди путь от имени таблицы до её файлов и понаблюдай, как форки появляются, а relfilenode меняется. Перед каждым шагом предсказывай: нулевой ли размер форка, совпадут ли OID и relfilenode.
Узнай путь к файлу:
SELECT pg_relation_filepath('flights');- разбери его на OID базы и relfilenode.Создай таблицу
forklab(id int, note text)и вставь 300 строк через generate_series.Предскажи и проверь размер карты видимости до очистки:
SELECT pg_relation_size('forklab','vm');(ожидай 0).Сравни OID и relfilenode:
SELECT oid, relfilenode, oid=relfilenode AS same FROM pg_class WHERE relname='forklab';- сейчас они равны.Выполни
VACUUM forklab;и снова посмотри размер_vm- предскажи, станет ли он ненулевым.Выполни
VACUUM FULL forklab;и снова сравни OID с relfilenode - объясни, почему OID тот же, а relfilenode другой.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Таблица хранится набором форков: главный (данные), _fsm (свободное место), _vm (видимость), _init (для UNLOGGED).
- pg_relation_filepath возвращает путь base/<oid базы>/<relfilenode> к главному форку.
- Главный форк есть всегда, служебные карты создаются лениво: _vm заполняет VACUUM.
- OID - постоянный идентификатор в каталоге; relfilenode - имя файла, меняется при перезаписи таблицы.
- VACUUM FULL, TRUNCATE, CLUSTER и смена типа колонки дают новый relfilenode при том же OID.
- Форк делится на сегменты по 1 ГБ: relfilenode, relfilenode.1, relfilenode.2 ...
Контрольные вопросы
Из каких файлов состоит таблица и зачем их несколько?
Показать ответ
Таблица состоит из форков: главного (строки),
_fsm(карта свободного места),_vm(карта видимости и заморозки) и_init(шаблон для нежурналируемых таблиц). Их разносят по файлам, чтобы служебные карты можно было читать и обновлять, не трогая основной массив данных: карты маленькие, и отдельный маленький файл дешевле, чем вплетать их в данные.Почему сразу после CREATE TABLE форка _vm на диске нет?
Показать ответ
Потому что форки создаются лениво - когда в них появляется смысл. Карту видимости заполняет
VACUUM, поэтому до первого вакуума размер_vmравен нулю (файла нет). Это видно поpg_relation_size('t','vm'): 0 до вакуума и ненулевое значение после. PostgreSQL не заводит структуру, пока она не понадобилась.Чем отличаются OID и relfilenode и когда они расходятся?
Показать ответ
OID - постоянный внутренний идентификатор отношения в системном каталоге. relfilenode - имя файла данных на диске. Сначала они равны, но команды, переписывающие таблицу целиком (
VACUUM FULL,TRUNCATE,CLUSTER, смена типа колонки), создают новый файл с новым relfilenode, оставляя OID прежним. После такой операции OID тот же, relfilenode - другой.Почему опасно сохранять relfilenode и обращаться к файлу по нему позже?
Показать ответ
Потому что relfilenode меняется при любой перезаписи таблицы. Сохранённое имя файла после
VACUUM FULLилиTRUNCATEстанет указывать на удалённый или чужой файл. Стабилен только OID; чтобы получить актуальный путь, нужно заново спроситьpg_relation_filepath. Имя файла - это снимок на момент запроса, а не постоянный адрес.Почему большая таблица на диске - это несколько файлов, а не один?
Показать ответ
Потому что форк делится на сегменты по 1 ГБ. Когда главный форк превышает гигабайт, данные продолжаются в файлах
relfilenode.1,relfilenode.2и так далее. Это защита от ограничений старых файловых систем на размер файла и удобство для операций с файлами. Логически это одна таблица, физически - набор сегментов.