5.1 Страница как единица обмена
PostgreSQL не читает с диска отдельные строки и не пишет отдельные байты. Он оперирует страницами фиксированного размера - по 8 КБ (8192 байта). Страницу ещё называют блоком. Это единица всего: чтения с диска, записи, кеширования в памяти, ссылок из журнала.
Фиксированный размер упрощает движку жизнь. Буферный кеш - это массив одинаковых слотов по 8 КБ. Журнал ссылается на страницу номером блока. Чтение идёт ровными порциями. За удобство платят внутренней фрагментацией: если строка чуть больше половины страницы, на страницу влезет только одна, и половина места пропадёт. Но для типичных строк это выгодный размен. Общая раскладка - в page-layout.
5.2 Четыре зоны страницы
Внутри страница разбита на четыре зоны:
Хитрость в том, что массив указателей растёт от начала вниз, а кортежи кладутся с конца вверх. Между ними - свободное место. Они движутся навстречу, и страница полна, когда встретились. Зачем так - разберём через пару разделов.
5.3 Заголовок: три числа, которые держат всё
Подключим линзу и прочитаем заголовок страницы 0 таблицы flights:
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- где заканчивается массив указателей. Всё до него - заголовок и указатели.upper- где начинаются кортежи. Всё после - данные строк.special- где начинается спецзона. У heap она пустая, поэтомуspecialравен размеру страницы.
5.4 Откуда берётся lower
Число lower = 224 не случайно. Заголовок занимает 24 байта. Дальше
идёт массив указателей по 4 байта на строку. У flights 50 строк,
значит указателей 50:
lower = 24 (заголовок) + 50 × 4 (указатели) = 224
Предскажи это перед запросом - и pageinspect подтвердит. Сами
указатели видны так:
SELECT lp, lp_off, lp_len
FROM heap_page_items(get_raw_page('flights', 0)) ORDER BY lp LIMIT 3;-- lp | lp_off | lp_len
-- ----+--------+--------
-- 1 | 8144 | 48
-- 2 | 8096 | 48
-- 3 | 8048 | 48
Указатели нумеруются 1, 2, 3 по возрастанию, а смещения lp_off
убывают: 8144, 8096, 8048. Кортежи заполняют страницу с конца. Подробно
про указатели - в line-pointers.
5.5 Свободное место посередине
Между концом указателей (lower) и началом кортежей (upper) лежит
свободное место. На нашей странице это 5464 - 224 = 5240 байт.
Когда вставляется новая строка, происходит две вещи навстречу: с
верхнего края добавляется новый указатель (растёт lower), а с нижнего
кладётся сам кортеж (убывает upper). Свободное место посередине
сжимается с обеих сторон. Как только его перестаёт хватать на новый
указатель и кортеж - страница считается полной, и строка идёт в другую
страницу.
Вот зачем указатели и кортежи разнесены по краям и растут навстречу: так свободное место всегда единым куском посередине, и не нужно искать дырки между записями.
5.6 Подводный камень: pd_lower больше pd_upper
lower всегда должен быть меньше или равен upper: указатели сверху,
кортежи снизу, между ними зазор. Если вдруг lower > upper - зоны
наложились, и это верный признак повреждённой страницы.
Такое не случается в нормальной работе: за этим следят и сам движок, и
контрольные суммы страниц, если они включены. Но если ты диагностируешь
подозрительную таблицу и page_header показывает lower больше
upper, дальше читать эту страницу как обычную нельзя - данные в ней
битые. Это первое, что проверяют при подозрении на порчу блока.
5.7 Зачем странице фиксированный размер
Вернёмся к началу: почему именно фиксированные 8 КБ. Потому что одинаковый размер пронизывает всю систему. Буферный кеш держит страницы в слотах одного размера - не нужно управлять кусками разной длины. Журнал предзаписи ссылается на изменения номером блока. Карта свободного места хранит по байту на страницу. Всё это работает, потому что страница - предсказуемая единица.
Размер 8 КБ - компромисс. Меньше - больше накладных расходов на заголовки и больше обращений к диску на тот же объём. Больше - грубее гранулярность блокировок и кеша, дороже частичные изменения. 8 КБ исторически легли в середину и стали значением по умолчанию. Менять его можно только при сборке, и почти никто этого не делает.
Уроки в sandbox
lab-5.1. Вскрытие страницы
Прочитай сырую страницу таблицы и назови её зоны - не из учебника, а из
вывода pageinspect. Перед каждым шагом предсказывай: сколько будет
указателей, чему равен lower.
Подключи линзу:
CREATE EXTENSION IF NOT EXISTS pageinspect;.Предскажи число строк на странице 0 таблицы
flightsи проверь:SELECT count(*) FROM heap_page_items(get_raw_page('flights', 0));.Предскажи
lowerпо формуле 24 + число_строк × 4, потом сверь:SELECT lower, upper, special FROM page_header(get_raw_page('flights', 0));.Посчитай свободное место как
upper - lowerи объясни, где оно лежит.Посмотри смещения первых строк:
SELECT lp, lp_off, lp_len FROM heap_page_items(get_raw_page('flights',0)) ORDER BY lp LIMIT 5;- убедись, что lp_off убывает.Объясни, почему
specialравенpagesize: у heap-страницы спецзона пустая.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Страница - фиксированные 8 КБ, единица чтения, записи, кеширования и ссылок из журнала.
- Четыре зоны: заголовок (24 байта), массив указателей, свободное место, кортежи; спецзона у heap пустая.
- lower - конец указателей, upper - начало кортежей; свободное место между ними.
- Указатели растут сверху вниз, кортежи - снизу вверх, навстречу друг другу.
- lower = 24 + число_строк × 4: заголовок плюс по 4 байта на указатель.
- lower > upper означает повреждённую страницу - зоны наложились.
- Фиксированный размер упрощает буферный кеш, журнал и карты; 8 КБ - компромисс по умолчанию.
Контрольные вопросы
Из каких четырёх зон состоит heap-страница и в каком порядке они идут?
Показать ответ
Сверху вниз: заголовок (24 байта с метаданными), массив указателей строк (растёт вниз), свободное место, кортежи (растут вверх от конца страницы). В самом конце - спецзона, у heap-таблицы пустая (у индексов она занята служебными данными метода доступа).
Почему lower равен 224 на странице с 50 строками?
Показать ответ
Потому что
lowerуказывает на конец массива указателей. Заголовок занимает 24 байта, затем идёт по 4 байта на каждый указатель строки. На 50 строк это 24 + 50 × 4 = 224. То есть всё до смещения 224 - это заголовок и указатели, а свободное место начинается после.Зачем указатели и кортежи разнесены по краям страницы и растут навстречу?
Показать ответ
Чтобы свободное место всегда было единым куском посередине. Указатели растут сверху вниз (растёт
lower), кортежи - снизу вверх (убываетupper), а между ними зазор. При вставке обе границы сдвигаются навстречу. Так не нужно искать дырки между записями: есть один непрерывный свободный участок, и проверка «влезет ли» сводится к сравнениюupper - lowerс нужным размером.О чём говорит ситуация lower > upper?
Показать ответ
О повреждении страницы. В норме
lower(конец указателей) всегда меньше или равенupper(началу кортежей) - между ними свободный зазор. Еслиlowerбольшеupper, зоны наложились, и данные в странице битые. В обычной работе такого не бывает; при диагностике подозрительной таблицы это первый признак порчи блока.Почему PostgreSQL использует страницы фиксированного размера?
Показать ответ
Потому что одинаковый размер упрощает всю систему: буферный кеш держит страницы в слотах одной длины, журнал ссылается на изменения номером блока, карта свободного места хранит по байту на страницу. 8 КБ - компромисс: меньше дало бы больше накладных расходов и обращений к диску, больше - грубее гранулярность кеша и блокировок и дороже частичные изменения. Менять размер можно только при сборке.