PostgreSQL читает и пишет диск не байтами и не строками, а страницами фиксированного размера - по 8 КБ (8192 байта). Страница (её ещё называют блоком) - это единица ввода-вывода, кеширования и блокировки. Понять раскладку одной страницы - значит понять, как вообще устроено хранение.
Внутри страница разбита на четыре зоны:
Хитрость в том, что массив указателей растёт от начала вниз, а кортежи кладутся с конца вверх. Между ними - свободное место. Они движутся навстречу друг другу, и страница считается полной, когда они встретились.
Заголовок: три числа, которые держат всё
Прочитать заголовок можно расширением pageinspect:
CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT lower, upper, special, pagesize
FROM page_header(get_raw_page('flights', 0));-- lower | upper | special | pagesize
-- -------+-------+---------+----------
-- 224 | 5464 | 8192 | 8192
lower- смещение, где заканчивается массив указателей. Всё до него - заголовок и указатели. На странице с 50 строками это(224 - 24) / 4 = 50указателей по 4 байта.upper- смещение, где начинаются кортежи. Всё после - данные строк.- Между
lowerиupperлежит свободное место. Здесь его5464 - 224 = 5240байт. specialуказывает на спецзону в конце. У heap-таблицы она пустая, поэтомуspecial = pagesize = 8192. У индексов там живут служебные данные метода доступа.
Если lower > upper - страница повреждена: зоны наложились. Это первый
признак битого блока.
Указатели и кортежи - почему врозь
Почему PostgreSQL не кладёт строки подряд, а заводит отдельный массив указателей? Потому что строка должна иметь стабильный адрес, даже когда физически двигается внутри страницы. Адрес строки - это номер указателя, а не смещение в байтах. Указатель хранит реальное смещение и может поменяться при уплотнении страницы, а внешний адрес строки остаётся тем же. Детали - в line-pointers.
Сами строки лежат как кортежи. У каждого свой заголовок с версионными полями - его разбирает tuple-header.
Зачем фиксированный размер
Фиксированные 8 КБ упрощают всё: буферный кеш оперирует одинаковыми слотами, журнал ссылается на блоки по номеру, чтение с диска идёт ровными порциями. Цена - внутренняя фрагментация: если строка чуть больше половины страницы, две строки на страницу не влезут, и часть места пропадёт. Для очень длинных значений есть отдельный механизм выноса - toast.