how/хранение

Анатомия страницы: 8 КБ, заполняется с двух концов

Таблица на диске - массив страниц по 8 КБ, и каждая заполняется сразу с двух сторон: массив указателей строк растёт сверху вниз от заголовка, кортежи растут снизу вверх от конца, а свободное место - это зазор между ними. Вот как заполняется одна страница.

PostgreSQL никогда не читает и не пишет таблицу построчно или побайтно. Его единица ввода-вывода, кеширования и блокировки - страница (её ещё называют блоком): фиксированные 8 КБ, 8192 байта. Файл таблицы - это просто массив таких страниц, пронумерованных с нуля.

Внутри у каждой страницы одни и те же четыре зоны:

  • заголовок 24 байта с LSN страницы, контрольной суммой и тремя смещениями: pd_lower, pd_upper, pd_special;
  • массив указателей строк по 4 байта, который растёт вниз от заголовка;
  • свободное место посередине;
  • кортежи (сами версии строк), которые растут вверх от конца страницы.

Хитрость в том, что массив указателей и кортежи движутся навстречу друг другу. Страница считается полной, как только они встретились. Указатель строки - это стабильный адрес строки (см. line-pointers), поэтому кортеж может двигаться внутри страницы, а адрес строки не меняется.

Нажми play и посмотри, как одна страница заполняется от пустой до полной за пять шагов.

step 1/5·00 · пустая страница: 8192 байта, заголовок 24 байта
08192заголовок · 24 Буказателисвободнокортежиспецзона · heap: пустоlsn · checksumlower · upper · specialpd_lowerконец указателейпустая страница: 8192 байта, почти всё - свободное место

§ шаги

  1. Только что выделенная страница почти целиком - свободное место. На самом верху сидит заголовок 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() перечисляет указатели и кортежи.

§ копнуть в базу знаний

§ попробовать руками