linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
  • PostgreSQL изнутри
    Страница и кортеж, MVCC, vacuum, WAL, планировщик и индексы
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
  • PostgreSQL изнутри
    Страница и кортеж, MVCC, vacuum, WAL, планировщик и индексы
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Cluster

← все кластеры

Страница, кортеж, TOAST, файлы отношения

Как PostgreSQL раскладывает данные на диске: страница 8 КБ, заголовок кортежа, указатели строк, TOAST для длинных значений, выравнивание полей и файлы отношения. Это фундамент - без него разговор про MVCC, vacuum и индексы повисает в воздухе.

7 вопросов · ~35 мин чтения

Questions

На этой странице

  1. 01Что лежит внутри страницы 8 КБ? Назови части и куда они растут.
  2. 02Что хранит заголовок версии строки? Перечисли поля и зачем они.
  3. 03Что такое ctid и почему на него нельзя смотреть как на стабильный идентификатор строки?
  4. 04Что такое TOAST, когда он срабатывает и какие у него стратегии хранения?
  5. 05Почему порядок колонок влияет на размер строки на диске?
  6. 06Что такое forks отношения и чем relfilenode отличается от oid?
  7. 07Где физически живёт таблица в каталоге кластера и как её там найти?

#page-8kb-anatomy

juniorчасто

Что лежит внутри страницы 8 КБ? Назови части и куда они растут.

Что отвечать

Страница - единица чтения и записи, по умолчанию 8 КБ. Внутри четыре зоны. Заголовок (`PageHeaderData`, 24 байта): контрольная сумма, LSN последней записи WAL, указатели `pd_lower` и `pd_upper`. Массив указателей строк (line pointers) - растёт от начала вниз. Сами версии строк - кладутся с конца вверх. Особая зона (special space) в конце - для индексов там служебные данные, в heap пустая. Свободное место - это дырка между `pd_lower` и `pd_upper`; когда она схлопывается, в страницу больше ничего не лезет.

Что хотят услышать

Senior должен: - назвать четыре зоны и сказать, что указатели и строки растут навстречу друг другу, а свободное место это зазор между ними - объяснить, зачем нужен слой указателей: строка может двигаться внутри страницы (после очистки), а внешний адрес `(page, item)` остаётся прежним - знать, что 8 КБ это compile-time параметр (`BLCKSZ`), менять его без пересборки нельзя - упомянуть `pageinspect` (`page_header`, `heap_page_items`) как способ посмотреть всё это на живой странице

Подводные камни

  • ✗ Сказать «строка лежит по фиксированному смещению» - смещение хранит line pointer, сама строка может переехать
  • ✗ Путать свободное место в странице с FSM: FSM это отдельная карта, а дырка `pd_lower..pd_upper` это место в конкретной странице
  • ✗ Думать, что 8 КБ можно поменять в конфиге - это параметр сборки `BLCKSZ`, не GUC

Follow-up

  • ? Что покажет `SELECT * FROM page_header(get_raw_page('t', 0))`?
  • ? Почему `pd_lower` растёт при INSERT, а строки идут с другого конца?
  • ? Зачем в странице контрольная сумма и когда она проверяется?

Глубина в базе знаний

  • Раскладка страницы 8 КБ
  • Line pointers и lp_flags
  • Заголовок кортежа
  • Free Space Map (карта свободного места)
tags: storage, page, heapbook: postgresql_internals-17.pdf:ch1 data organization · postgresql_internals-17.pdf:ch3 pages and tuples

#tuple-header-fields

intermediateчасто

Что хранит заголовок версии строки? Перечисли поля и зачем они.

Что отвечать

Перед пользовательскими данными у каждой версии строки лежит заголовок `HeapTupleHeader`, 23 байта плюс выравнивание. Главные поля: `t_xmin` - номер транзакции, создавшей версию; `t_xmax` - номер транзакции, удалившей или заблокировавшей её (0, если жива); `t_ctid` - указатель на следующую версию этой строки (для цепочки UPDATE) или на саму себя; `t_infomask` и `t_infomask2` - биты состояния (закоммичена ли xmin/xmax, есть ли NULL'ы, HOT и прочее); `t_hoff` - смещение, где кончается заголовок с битовой картой NULL'ов и начинаются данные.

Что хотят услышать

Senior должен: - связать `t_xmin`/`t_xmax` с видимостью: именно по ним и по снимку решается, видна ли версия текущей транзакции - объяснить hint bits в `t_infomask`: они кешируют «xmin закоммичена» и избавляют от повторного похода в clog - сказать, что `t_ctid` указывает на следующую версию, и так строится цепочка версий при UPDATE - знать, что NULL'ы хранятся не значением, а битовой картой сразу за фиксированной частью заголовка, поэтому `t_hoff` плавает

Подводные камни

  • ✗ Сказать «xmax это удаление» и забыть, что xmax ставится и при блокировке строки `SELECT FOR UPDATE`
  • ✗ Думать, что NULL занимает место под значение - он отмечен битом в null bitmap, данных под него нет
  • ✗ Считать заголовок фиксированным - его длина `t_hoff` зависит от наличия null bitmap и OID

Follow-up

  • ? Что в `t_infomask` отличает закоммиченную транзакцию от ещё идущей?
  • ? Куда указывает `t_ctid` у самой свежей версии строки?
  • ? Почему два разных INSERT в одну строку дают разный `t_hoff`?

Глубина в базе знаний

  • Заголовок кортежа
  • xmin, xmax и правила видимости
  • Line pointers и lp_flags
  • clog и подсказки фиксации (hint bits)
tags: storage, tuple, mvccbook: postgresql_internals-17.pdf:ch3 pages and tuples

#ctid-stability

intermediateиногда

Что такое ctid и почему на него нельзя смотреть как на стабильный идентификатор строки?

Что отвечать

`ctid` - физический адрес версии строки: пара `(номер страницы, номер указателя)`. Он точно говорит, где сейчас лежит версия, и его удобно использовать внутри одного запроса. Но при любом UPDATE рождается новая версия с новым `ctid`, а старая остаётся до очистки. После `VACUUM FULL`, `CLUSTER` или обычной очистки с дефрагментацией адреса вообще переезжают. Поэтому `ctid` нельзя класть в приложение как ключ строки - для этого есть первичный ключ.

Что хотят услышать

Senior должен: - чётко сказать: `ctid` адресует версию, а не логическую строку, и меняется на каждом UPDATE - привести законные применения: дедупликация по `ctid` в рамках запроса, точечное удаление дублей, ручная диагностика «где физически строка» - объяснить, почему `WHERE ctid = ...` между транзакциями небезопасно: очистка и перемещения сделают адрес невалидным - связать с HOT: HOT-цепочка живёт внутри страницы, и `ctid` старой версии ведёт к новой по `t_ctid`

Подводные камни

  • ✗ Закешировать `ctid` в приложении и потом по нему обновлять строку - после vacuum попадёшь не туда или в никуда
  • ✗ Считать, что `ctid` монотонный или отражает порядок вставки - нет, очистка переиспользует слоты указателей
  • ✗ Путать `ctid` (адрес версии) с `oid` или первичным ключом (логический идентификатор)

Follow-up

  • ? Как удалить дубликаты строк, оставив по одной, используя `ctid`?
  • ? Почему после `VACUUM FULL` все `ctid` в таблице меняются?
  • ? Чем `ctid` отличается от системного `oid`?

Глубина в базе знаний

  • Line pointers и lp_flags
  • Заголовок кортежа
  • HOT-обновления и fillfactor
tags: storage, ctid, mvccbook: postgresql_internals-17.pdf:ch3 pages and tuples · postgresql_internals-17.pdf:ch5 hot updates

#toast-mechanics

intermediateчасто

Что такое TOAST, когда он срабатывает и какие у него стратегии хранения?

Что отвечать

Версия строки обязана влезать в страницу 8 КБ, а значения бывают длиннее. TOAST (The Oversized-Attribute Storage Technique) выносит длинные поля наружу: сначала пытается сжать, если всё ещё велико - режет на куски и кладёт в служебную TOAST-таблицу, а в строке оставляет указатель. Порог - примерно 2 КБ на строку (`TOAST_TUPLE_THRESHOLD`). Стратегии на колонку: `plain` (не трогать, только для коротких типов), `extended` (сжать и при нужде вынести, дефолт для `text`/`jsonb`), `external` (вынести без сжатия), `main` (сжать, выносить в последнюю очередь).

Что хотят услышать

Senior должен: - объяснить два независимых шага: сжатие и вынос наружу, и что стратегия управляет обоими - понимать, почему `external` помогает substring/поиску по началу: без сжатия можно читать куски, не распаковывая всё значение - знать, что TOAST-таблица своя у каждой таблицы и тоже подлежит vacuum и собственному раздуванию - связать с производительностью: пока длинное поле не трогается в SELECT, оно не читается из TOAST (отложенная детоастизация)

Подводные камни

  • ✗ Думать, что TOAST включают вручную - он работает сам, как только строка не влезает в страницу
  • ✗ Забыть, что частые UPDATE больших jsonb создают мусор и в основной, и в TOAST-таблице
  • ✗ Считать, что `SELECT count(*)` читает TOAST - длинные значения не детоастятся, пока их не запросили

Follow-up

  • ? Чем стратегия `external` отличается от `extended` для колонки `text`?
  • ? Где физически лежит TOAST-таблица для таблицы `documents`?
  • ? Почему длинный `jsonb` под частыми UPDATE раздувается вдвойне?

Глубина в базе знаний

  • TOAST: вынос и сжатие длинных значений
  • Раскладка страницы 8 КБ
  • Выравнивание и порядок колонок
tags: storage, toast, jsonbbook: postgresql_internals-17.pdf:ch1 data organization

#column-order-padding

seniorиногда

Почему порядок колонок влияет на размер строки на диске?

Что отвечать

Поля фиксированной длины в версии строки выравниваются по своей границе: `bigint` и `double` - по 8 байт, `int` - по 4, `smallint` - по 2. Если за `boolean` (1 байт) сразу идёт `bigint`, между ними добавляется 7 байт паддинга, чтобы `bigint` лёг по адресу, кратному 8. Сгруппируешь широкие поля впереди, а узкие (`bool`, `smallint`) в хвосте - дырок будет меньше и строка займёт меньше байт. На таблице в сотни миллионов строк это реальные гигабайты и лишние страницы для чтения.

Что хотят услышать

Senior должен: - объяснить причину: процессорное выравнивание (alignment) типов, а не прихоть PostgreSQL - дать практическое правило: класть колонки по убыванию ширины выравнивания, переменные (`text`, `numeric`) - в конце - оценивать эффект через `pageinspect` (`lp_len`) или `pg_column_size`, а не на глаз - понимать, что выигрыш не только в диске: меньше байт на строку - больше строк в странице - меньше страниц в буферном кеше и в чтениях

Подводные камни

  • ✗ Считать паддинг микрооптимизацией - на больших таблицах это десятки процентов размера
  • ✗ Думать, что перестановка колонок в существующей таблице сожмёт её - нужен пересоздать или `VACUUM FULL`/`CLUSTER`
  • ✗ Забывать про `NUMERIC` и `text`: у них выравнивание по 4 или по 1, и правило для них другое

Follow-up

  • ? Как измерить размер одной строки до и после перестановки колонок?
  • ? Почему `(int, bigint, int)` тяжелее, чем `(bigint, int, int)`?
  • ? Меняет ли перестановка колонок что-то для уже записанных строк?

Глубина в базе знаний

  • Выравнивание и порядок колонок
  • Заголовок кортежа
  • Раскладка страницы 8 КБ
tags: storage, alignment, paddingbook: postgresql_internals-17.pdf:ch3 pages and tuples

#relfilenode-forks

intermediateиногда

Что такое forks отношения и чем relfilenode отличается от oid?

Что отвечать

Каждое отношение на диске - это не один файл, а несколько слоёв (forks). Основной слой (main) хранит сами страницы с данными. FSM (free space map) - карта свободного места по страницам. VM (visibility map) - битовая карта «все версии в странице видны всем» и «все заморожены». Init - пустой шаблон для unlogged-таблиц. Имя файлов задаёт `relfilenode`, а не `oid`: `oid` - стабильный идентификатор объекта в каталоге, а `relfilenode` - имя текущего набора файлов. Команды вроде `TRUNCATE`, `VACUUM FULL`, `REINDEX` меняют `relfilenode`, оставляя `oid` прежним.

Что хотят услышать

Senior должен: - назвать слои main/fsm/vm/init и сказать, что fsm и vm появляются не сразу, а после первой очистки - различить `oid` (логический, в `pg_class`) и `relfilenode` (физический, имя файла), и привести операции, которые их рассинхронизируют - знать про сегментацию: файл слоя режется на куски по 1 ГБ (`.1`, `.2`, ...) - уметь найти файл: `pg_relation_filepath('t')` отдаёт путь от каталога кластера

Подводные камни

  • ✗ Считать, что имя файла таблицы равно её `oid` - после `VACUUM FULL` это уже не так
  • ✗ Думать, что FSM и VM есть всегда - у только что созданной таблицы их нет до первого vacuum
  • ✗ Забыть про сегменты по 1 ГБ и искать одну таблицу в одном файле

Follow-up

  • ? Что вернёт `pg_relation_filepath('orders')` и из чего состоит путь?
  • ? Почему у новой таблицы нет файла `_fsm`, а после `VACUUM` он появляется?
  • ? Какие операции меняют `relfilenode`, не трогая `oid`?

Глубина в базе знаний

  • relfilenode и форки отношения
  • Free Space Map (карта свободного места)
  • Visibility Map (карта видимости)
  • Кластер, PGDATA и каталоги
tags: storage, forks, relfilenodebook: postgresql_internals-17.pdf:ch1 data organization

#where-table-on-disk

juniorиногда

Где физически живёт таблица в каталоге кластера и как её там найти?

Что отвечать

Внутри `PGDATA` основные данные лежат в `base/<oid базы>/<relfilenode>`. Каждая база - свой подкаталог по `oid`, каждое отношение - файлы по `relfilenode`. Большой слой нарезан на сегменты по 1 ГБ. Табличные пространства (`tablespace`) выносят отдельные объекты на другой диск: тогда вместо `base/` используется каталог из `pg_tblspc/`. Найти путь проще всего через `pg_relation_filepath('имя')`, а размеры - через `pg_relation_size` и `pg_total_relation_size` (последний считает с индексами и TOAST).

Что хотят услышать

Senior должен: - описать иерархию `base/<db>/<relfilenode>` и роль табличных пространств - различать `pg_relation_size` (только main-слой), `pg_table_size` (с TOAST и fsm/vm) и `pg_total_relation_size` (плюс индексы) - понимать, что одно отношение это много файлов: сегменты плюс forks - не лезть руками в файлы кластера на работающем сервере - чтение и запись идут через буферный кеш, прямые правки рушат целостность

Подводные камни

  • ✗ Считать, что одна таблица это один файл - это набор сегментов и слоёв
  • ✗ Путать `oid` базы (каталог) и `relfilenode` отношения (файл)
  • ✗ Мерить размер `pg_relation_size` и удивляться, что он меньше реального - он без индексов и TOAST

Follow-up

  • ? Чем `pg_total_relation_size` отличается от `pg_relation_size`?
  • ? Куда переедут файлы, если перенести таблицу в другой tablespace?
  • ? Почему нельзя редактировать файлы в `base/` на живом сервере?

Глубина в базе знаний

  • Кластер, PGDATA и каталоги
  • relfilenode и форки отношения
  • TOAST: вынос и сжатие длинных значений
tags: storage, pgdata, tablespacebook: postgresql_internals-17.pdf:ch1 data organization
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.