# Раскладка страницы 8 КБ _Хранение и формат на диске · PostgreSQL Knowledge Base_ **TL;DR:** Таблица на диске - это массив страниц по 8 КБ. Внутри страницы четыре зоны: заголовок 24 байта, массив указателей строк (растёт сверху вниз), свободное место и сами кортежи (растут снизу вверх). У heap-страницы спецзона в конце пустая. Указатели и кортежи движутся навстречу. PostgreSQL читает и пишет диск не байтами и не строками, а **страницами** фиксированного размера - по 8 КБ (8192 байта). Страница (её ещё называют блоком) - это единица ввода-вывода, кеширования и блокировки. Понять раскладку одной страницы - значит понять, как вообще устроено хранение. Внутри страница разбита на четыре зоны: ```mermaid flowchart TB H["Заголовок, 24 байта: lsn, lower, upper, special"] LP["Указатели строк: массив по 4 байта, растёт вниз"] FS["Свободное место"] T["Кортежи: растут вверх от конца страницы"] SP["Спецзона: у heap пустая, у индексов занята"] H --> LP --> FS --> T --> SP ``` Хитрость в том, что массив указателей растёт от начала вниз, а кортежи кладутся с конца вверх. Между ними - свободное место. Они движутся навстречу друг другу, и страница считается полной, когда они встретились. ## Заголовок: три числа, которые держат всё Прочитать заголовок можно расширением `pageinspect`: ```sql 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](/courses/postgres/kb/line-pointers.md). Сами строки лежат как кортежи. У каждого свой заголовок с версионными полями - его разбирает [tuple-header](/courses/postgres/kb/tuple-header.md). ## Зачем фиксированный размер Фиксированные 8 КБ упрощают всё: буферный кеш оперирует одинаковыми слотами, журнал ссылается на блоки по номеру, чтение с диска идёт ровными порциями. Цена - внутренняя фрагментация: если строка чуть больше половины страницы, две строки на страницу не влезут, и часть места пропадёт. Для очень длинных значений есть отдельный механизм выноса - [toast](/courses/postgres/kb/toast.md). ## Команды ```sql CREATE EXTENSION IF NOT EXISTS pageinspect; ``` Подключить линзы для чтения сырых страниц ```sql SELECT * FROM page_header(get_raw_page('flights', 0)); ``` Заголовок страницы 0: lsn, lower, upper, special, prune_xid ```sql SELECT lp, lp_off, lp_len FROM heap_page_items(get_raw_page('flights', 0)); ``` Указатели и смещения всех строк на странице 0 ```sql SELECT relpages FROM pg_class WHERE relname = 'flights'; ``` Сколько страниц занимает таблица по оценке планировщика ## См. также - [Line pointers и lp_flags](/courses/postgres/kb/line-pointers.md) - [Заголовок кортежа](/courses/postgres/kb/tuple-header.md) - [TOAST: вынос и сжатие длинных значений](/courses/postgres/kb/toast.md) - [relfilenode и форки отношения](/courses/postgres/kb/relfilenode-forks.md)