Базы данных
Heap-хранилище, TOAST и выравнивание колонок
Команда сокращает таблицу с 40 колонок до 35. Хранилище уменьшается на 18%. Они не удалили никаких данных — они переупорядочили оставшиеся колонки, чтобы устранить alignment padding. База данных никогда не меняла своих правил. Команда наконец их прочитала.
Heap: как Postgres хранит строки
Postgres хранит строки таблиц в фиксированных страницах 8КБ на диске. Структура важна для хранения и производительности запросов:
- Каждая страница имеет заголовок (24 байта) и массив указателей на элементы.
- Каждая строка (кортеж) начинается с 23-байтового заголовка кортежа, содержащего информацию о видимости транзакции (xmin, xmax, ctid — разбирается в уроке про MVCC).
- Если любая колонка nullable, следует null bitmap (1 бит на колонку, с выравниванием до байта).
- Затем данные колонок в порядке объявления, каждая дополнена до своей границы выравнивания.
Общий размер строки влияет на то, сколько строк помещается на одну страницу, что влияет на количество страниц, которые нужно прочитать при последовательном сканировании — одна из самых прямых связей между дизайном схемы и стоимостью запросов.
| Тип Postgres | Размер хранения | Выравнивание | Примечания |
|---|---|---|---|
| SMALLINT | 2 байта | 2 байта | Диапазон: ±32 767 |
| INTEGER | 4 байта | 4 байта | Диапазон: ±2.1B |
| BIGINT / BIGSERIAL | 8 байт | 8 байт | Диапазон: ±9.2×10¹⁸ |
| UUID | 16 байт | 4 байта | Нативный тип, не TEXT |
| BOOLEAN | 1 байт | 1 байт | True/false/null |
| TIMESTAMPTZ | 8 байт | 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). Что происходит:
- Широкое значение сжимается (LZ4 или pglz по умолчанию).
- Если всё ещё превышает порог, нарезается на чанки ~2КБ и хранится в отдельной TOAST-таблице рядом с основной таблицей.
- Основная строка содержит указатель (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?
- 01Опишите механизм TOAST: когда он срабатывает, что делает с данными, и два следствия для производительности при дизайне схемы.
- 02Почему UUIDv4 — плохой выбор для primary key на большой таблице, и как UUIDv7 решает эту проблему?
- 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
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior