how/хранение
Анатомия страницы: 8 КБ, заполняется с двух концов
Таблица на диске - массив страниц по 8 КБ, и каждая заполняется сразу с двух сторон: массив указателей строк растёт сверху вниз от заголовка, кортежи растут снизу вверх от конца, а свободное место - это зазор между ними. Вот как заполняется одна страница.
PostgreSQL никогда не читает и не пишет таблицу построчно или побайтно. Его единица ввода-вывода, кеширования и блокировки - страница (её ещё называют блоком): фиксированные 8 КБ, 8192 байта. Файл таблицы - это просто массив таких страниц, пронумерованных с нуля.
Внутри у каждой страницы одни и те же четыре зоны:
- заголовок 24 байта с LSN страницы, контрольной суммой и тремя смещениями:
pd_lower,pd_upper,pd_special; - массив указателей строк по 4 байта, который растёт вниз от заголовка;
- свободное место посередине;
- кортежи (сами версии строк), которые растут вверх от конца страницы.
Хитрость в том, что массив указателей и кортежи движутся навстречу друг другу. Страница считается полной, как только они встретились. Указатель строки - это стабильный адрес строки (см. line-pointers), поэтому кортеж может двигаться внутри страницы, а адрес строки не меняется.
Нажми play и посмотри, как одна страница заполняется от пустой до полной за пять шагов.
§ шаги
Только что выделенная страница почти целиком - свободное место. На самом верху сидит заголовок 24 байта:
pd_lsn последняя WAL-запись, тронувшая страницу
pd_checksum необязательная контрольная сумма
pd_lower 24 -> конец массива указателей
pd_upper 8192 -> начало кортежей
pd_special 8192 -> спецзона (у heap пустая)
На пустой странице
pd_lowerстоит сразу за заголовком, аpd_upper- в самом конце, так что зазор между ними - вся страница. Всё дальнейшее - это движение этих двух чисел навстречу.
итого
Что важно запомнить:
- Страница - это 8192 байта, единица ввода-вывода и зерно буферного кеша (см. buffer-cache). Таблица - массив страниц; индекс - другая раскладка тех же блоков по 8 КБ.
- Два курсора задают раскладку:
pd_lower- где кончается массив указателей,pd_upper- где начинаются кортежи. Свободное место - это ровноpd_upper - pd_lower, а карта свободного места ведёт его по страницам (см. free-space-map), чтобы INSERT нашёл страницу с местом. - Адрес строки - это
ctidвида(блок, указатель), а не смещение в байтах. Указатель хранит реальное смещение и может сдвинуться при уплотнении страницы, а адрес строки остаётся тем же. Подробности - в line-pointers. - У кортежа есть собственный заголовок с MVCC-полями (см. tuple-header), поэтому у одной логической строки на странице может быть сразу несколько версий.
- Спецзона в самом конце у heap-таблицы пустая; методы доступа индексов кладут туда свои служебные данные. Когда
pd_lowerвстречаетpd_upper, следующая строка уходит на новую страницу, а слишком большое для страницы значение выносится в TOAST (см. toast).
Всё это можно прочитать вживую расширением pageinspect: page_header() отдаёт три смещения, heap_page_items() перечисляет указатели и кортежи.
§ копнуть в базу знаний
- page-layoutраскладка страницы - четыре зоны страницы 8 КБ
- line-pointersуказатели строк - стабильный адрес строки
- tuple-headerзаголовок кортежа - системные поля версии
- free-space-mapкарта свободного места - на какой странице есть место
- toastTOAST - куда уходят слишком длинные значения
- relfilenode-forksrelfilenode и форки - файлы за таблицей