awesome-everything EN
↑ Обратно к восхождению

Базы данных

Heap-хранилище, TOAST и выравнивание колонок

Суть Как Postgres хранит строки в страницах 8КБ, что TOAST делает с широкими значениями, почему порядок объявления колонок влияет на размер хранилища, и как выбирать типы данных Postgres на уровне байт.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Команда сокращает таблицу с 40 колонок до 35. Хранилище уменьшается на 18%. Они не удалили никаких данных — они переупорядочили оставшиеся колонки, чтобы устранить alignment padding. База данных никогда не меняла своих правил. Команда наконец их прочитала.

Heap: как Postgres хранит строки

Postgres хранит строки таблиц в фиксированных страницах 8КБ на диске. Структура важна для хранения и производительности запросов:

  • Каждая страница имеет заголовок (24 байта) и массив указателей на элементы.
  • Каждая строка (кортеж) начинается с 23-байтового заголовка кортежа, содержащего информацию о видимости транзакции (xmin, xmax, ctid — разбирается в уроке про MVCC).
  • Если любая колонка nullable, следует null bitmap (1 бит на колонку, с выравниванием до байта).
  • Затем данные колонок в порядке объявления, каждая дополнена до своей границы выравнивания.

Общий размер строки влияет на то, сколько строк помещается на одну страницу, что влияет на количество страниц, которые нужно прочитать при последовательном сканировании — одна из самых прямых связей между дизайном схемы и стоимостью запросов.

Тип PostgresРазмер храненияВыравниваниеПримечания
SMALLINT2 байта2 байтаДиапазон: ±32 767
INTEGER4 байта4 байтаДиапазон: ±2.1B
BIGINT / BIGSERIAL8 байт8 байтДиапазон: ±9.2×10¹⁸
UUID16 байт4 байтаНативный тип, не TEXT
BOOLEAN1 байт1 байтTrue/false/null
TIMESTAMPTZ8 байт8 байтТочность до микросекунды, хранится UTC
NUMERIC(p,s)Переменный (2-1000 байт)4 байтаТочный; используйте для денег
TEXT / VARCHARПеременный (1 байт + содержимое)4 байтаНет разницы в хранении; TEXT предпочтительнее
JSONBПеременный (бинарный)4 байтаTOAST-способный свыше ~2КБ

Alignment padding для колонок

Alignment padding вставляется между колонками, чтобы каждое значение начиналось на границе своего выравнивания. Порядок колонок, чередующий широкие и узкие типы, тратит байты:

-- Плохой порядок: [SMALLINT(2), BIGINT(8), SMALLINT(2)]
-- Структура: 2 байта + 6 байт padding + 8 байт + 2 байта + 6 байт padding = 24 байта

-- Хороший порядок: [BIGINT(8), SMALLINT(2), SMALLINT(2)]
-- Структура: 8 байт + 2 байта + 2 байта + 4 байта padding = 16 байт

На широкой таблице (40 колонок смешанных типов) плохой порядок колонок может тратить 10-20% хранилища. ORM-сгенерированные схемы редко оптимизируют это; это реальный производственный рычаг на таблицах свыше ~100M строк.

Практическое правило: объявляйте колонки от самого широкого выравнивания к самому узкому (сначала 8-байтовые типы, затем 4-байтовые, затем 2-байтовые, затем 1-байтовые). Типы переменной длины (TEXT, JSONB) идут последними — у них внутренние заголовки длины и их стоимость выравнивания фиксирована независимо от позиции относительно колонок фиксированной ширины.

TOAST: обработка широких значений

Страницы Postgres — 8КБ. Строки, превышающие примерно 2КБ, активируют TOAST (The Oversized-Attribute Storage Technique). Что происходит:

  1. Широкое значение сжимается (LZ4 или pglz по умолчанию).
  2. Если всё ещё превышает порог, нарезается на чанки ~2КБ и хранится в отдельной TOAST-таблице рядом с основной таблицей.
  3. Основная строка содержит указатель (18 байт), ссылающийся на TOAST-чанки.

TOAST прозрачен — SELECT возвращает полное значение как если бы оно было inline — но имеет последствия для производительности:

  • Чтение TOAST-колонки требует дополнительного обращения к диску из TOAST-таблицы (не из основной страницы).
  • Обновление TOAST-значения перезаписывает чанки side-таблицы плюс указатель в основной строке.
  • Перечисление строк без чтения TOAST-колонок дёшево — указатель inline, чанки не получаются если вы не SELECT эту колонку.

Импликация для дизайна схемы: если широкая TEXT/JSONB/BYTEA колонка редко читается (это прикреплённые метаданные, которые показываются только на странице деталей), важно исключать её из запросов, перечисляющих строки. SELECT * FROM events LIMIT 1000 получает TOAST payload для каждого события — SELECT id, event_type, created_at FROM events LIMIT 1000 не делает этого.

Почему это работает

TOAST был введён для сохранения модели страниц 8КБ без жёсткого ограничения размера строки. Он в основном прозрачен для кода приложения, но виден в выводе EXPLAIN (TOAST-таблица появляется как отдельное отношение в плане если запрос читает TOAST-колонки). Компромисс дизайна: единообразие страниц (все страницы 8КБ, предсказуемый I/O) за счёт случайных дополнительных обращений для широких колонок.

Выбор типов: продакшн-дефолты

Целочисленные типы. Выбирайте минимальный диапазон, превышающий ваш домен в 100 раз. user_id, который никогда не превысит 10 миллионов, может быть INTEGER (4 байта, экономит 4 байта на запись индекса). ID платежа, который может достичь миллиардов, должен быть BIGINT. Никогда SMALLINT для чего-либо, что может вырасти.

Строковые типы. TEXT — продакшн-дефолт. VARCHAR(n) добавляет ограничение проверки длины, но использует то же хранилище. CHAR(n) дополняет до длины (пробелы вызывают тонкие баги) — почти всегда неверный выбор.

Деньги. NUMERIC(p,s) (точная арифметика) или BIGINT, хранящий центы. Никогда REAL или DOUBLE PRECISION — IEEE 754 float накапливает ошибки округления в финансовой арифметике. Колонка REAL для денег дрейфует на центы после месяцев транзакций.

Временные метки. TIMESTAMPTZ (с часовым поясом) — продакшн-дефолт: хранит UTC, отображает в timezone сессии. TIMESTAMP (без зоны) — ловушка при работе приложения в разных часовых поясах. DATE для дат без времени.

UUID vs BIGSERIAL. BIGSERIAL занимает 8 байт, последователен (удобен для B-tree, хорошая локальность индекса), прост. UUID занимает 16 байт, глобально уникален (подходит для распределённых вставок, multi-region, offline-first клиентов). UUIDv4 случаен — B-tree индексы плохо фрагментируются на больших таблицах. UUIDv7 (упорядоченный по времени, RFC 9562) решает проблему локальности с временным префиксом; это современный дефолт когда нужен UUID. Выбирайте BIGSERIAL для single-region сервисов; UUIDv7 для multi-region или offline-first.

Викторина

Команда выполняет `SELECT * FROM events ORDER BY created_at DESC LIMIT 100` на таблице в 50M строк и замечает что запрос в 3 раза медленнее ожидаемого несмотря на индекс по created_at. Таблица events имеет большую JSONB-колонку payload со средним размером 8КБ. Наиболее вероятная причина?

Выбери лучший вариант

Новому сервису нужен глобально уникальный идентификатор строки для таблицы orders. Сервис сейчас single-region, но может расшириться до multi-region через 18 месяцев.

Викторина

Широкая таблица events объявила колонки в таком порядке: (id BIGSERIAL, created_at TIMESTAMPTZ, type SMALLINT, flags BOOLEAN, user_id BIGINT, details TEXT). Какое переупорядочение уменьшает alignment padding?

Вспомните перед уходом
  1. 01
    Опишите механизм TOAST: когда он срабатывает, что делает с данными, и два следствия для производительности при дизайне схемы.
  2. 02
    Почему UUIDv4 — плохой выбор для primary key на большой таблице, и как UUIDv7 решает эту проблему?
  3. 03
    Назовите три выбора типов, являющихся продакшн-дефолтами в Postgres, и объясните что стоит неверная альтернатива.
Итог

Строки Postgres хранятся в 8КБ страницах: 23-байтовый заголовок кортежа, опциональный null bitmap, затем данные колонок с alignment padding до границ выравнивания. Порядок объявления колонок влияет на padding — объявляйте фиксированные колонки самыми широкими первыми (8-байтовые, затем 4-байтовые, затем 2-байтовые, затем 1-байтовые, затем переменной длины). Значения шире ~2КБ TOAST-ируются в side-таблицу; SELECT * всегда их получает. Для выбора типов: TIMESTAMPTZ (не TIMESTAMP), NUMERIC или BIGINT-cents (не REAL/FLOAT для денег), TEXT (не CHAR(n)), BIGSERIAL для single-region PK, UUIDv7 для глобально уникальных распределённых ключей. Это решения схемы, которые трудно изменить на больших таблицах — принимайте их правильно с самого начала.

Связанные уроки
встречается в140
Продолжить восхождение ↑Целостность схемы: deferral, версионирование и сбои в продакшне
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.