Базы данных
Целостность схемы: deferral, версионирование и сбои в продакшне
Команда запускает пакетную миграцию — 3 миллиона строк перепривязываются к новым родителям. На строке 2 847 221 срабатывает нарушение foreign key. Транзакция откатывается. Все три миллиона обновлений отменяются. Миграция запускается повторно в 3 ночи. То же нарушение. Никто не проверил наличие сиротских строк перед стартом.
Применение переходов состояний
SQL CHECK constraints вычисляются при каждой записи и должны быть детерминированы. Они могут обращаться только к другим колонкам той же строки, но не к другим строкам. Это значит, что CHECK на уровне колонки может проверить «статус должен быть одним из этих значений», но не «завершённый заказ не может вернуться в pending».
Для инвариантов переходов состояний правильные инструменты:
Row-level triggers. BEFORE UPDATE триггер, бросающий исключение при недопустимом переходе:
CREATE OR REPLACE FUNCTION enforce_order_transitions()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status = 'completed' AND NEW.status != 'completed' THEN
RAISE EXCEPTION 'order % cannot leave completed state', OLD.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_order_status
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION enforce_order_transitions();Сгенерированные колонки с CHECK. Колонка is_final типа GENERATED ALWAYS AS (status IN ('completed','cancelled')) STORED плюс триггер или проверка на уровне приложения, запрещающая обновление строк, где OLD.is_final = true. Сгенерированная колонка документирует правило финальности прямо в схеме.
Row-level security. RLS-политики Postgres могут кодировать правила доступа, зависящие от состояния строки — например, USING (status != 'archived') на UPDATE запрещает обновление архивных строк без проверок на уровне приложения.
Дисциплина: бизнес-правила, которые можно выразить декларативно (в CHECK или RLS), живут в схеме. Правила, требующие процедурной логики (переходы состояний, cross-row проверки), живут в триггерах. Применение только на уровне приложения — наиболее слабый вариант: каждый потребитель должен знать правила.
Отложенные constraints
По умолчанию Postgres проверяет constraints после каждого отдельного выражения. Для некоторых сложных многотабличных операций промежуточные состояния нарушали бы constraint, хотя конечное состояние корректно.
-- Circular FK: A ссылается на B, B ссылается на A
-- Вставить A требует существования B; вставить B требует существования A
-- Ни то, ни другое нельзя вставить первым при IMMEDIATE constraints
ALTER TABLE a ALTER CONSTRAINT fk_a_to_b DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE b ALTER CONSTRAINT fk_b_to_a DEFERRABLE INITIALLY DEFERRED;
BEGIN;
INSERT INTO a (id, b_id) VALUES (1, 1); -- b_id=1 ещё не существует; deferred: OK
INSERT INTO b (id, a_id) VALUES (1, 1); -- a_id=1 уже существует; deferred check: consistent
COMMIT; -- Обе FK-проверки выполняются здесь; обе проходятDEFERRABLE INITIALLY DEFERRED означает отложенную проверку до COMMIT. DEFERRABLE INITIALLY IMMEDIATE объявляет constraint отложенным, но немедленным по умолчанию — можно переключить в рамках транзакции через SET CONSTRAINTS ... DEFERRED.
Используйте deferral экономно. Отложенные constraints проверяются при COMMIT. Нарушение откатывает всю транзакцию без промежуточного указания — в отличие от немедленного constraint, который проваливает конкретное выражение. Это усложняет отладку. Продакшн-гигиена: используйте DEFERRABLE только для FK с реальными циклическими или многошаговыми зависимостями, и только в транзакциях, где временное нарушение хорошо понято.
| Режим | Когда проверяется | Точка сбоя | Применение |
|---|---|---|---|
| IMMEDIATE (по умолчанию) | После каждого выражения | На нарушающем выражении | Все стандартные constraints |
| DEFERRABLE INITIALLY IMMEDIATE | После каждого выражения (если не переопределено) | На нарушающем выражении | Условно отложенный; редко |
| DEFERRABLE INITIALLY DEFERRED | При COMMIT | Транзакция откатывается при COMMIT | Circular FK, граф-реповязки |
Версионирование схемы: expand-then-contract
Реляционная схема — это контракт между базой и каждым потребителем. Добавление nullable-колонки не нарушает контракт. Добавление NOT NULL-колонки без дефолта ломает каждый INSERT. Удаление колонки ломает каждого читателя. Переименование ломает обоих.
Зрелые команды управляют схемой как кодом — файлы миграций в системе контроля версий (Flyway, Liquibase, golang-migrate, Prisma Migrate, sqlx) — и следуют паттерну expand-then-contract для ломающих изменений:
- Expand: добавить новую форму рядом со старой. Новая колонка nullable, никаких изменений в существующем коде.
- Задеплоить код, пишущий в обе. Приложение пишет и в старую, и в новую колонку. Старая всё ещё читается.
- Backfill новой колонки для существующих строк (пакетами, чтобы не держать долгие блокировки).
- Задеплоить код, читающий из новой. Приложение читает из новой, откатывается на старую в переходный период.
- Проверить паритет. Запрос сравнивает старые и новые значения; ноль расхождений перед продолжением.
- Contract: удалить старую форму в последующей миграции.
Паттерн применяется к переименованиям колонок, изменениям типов и разбиению таблиц. Добавляет один цикл деплоя на каждое ломающее изменение; альтернатива — даунтайм или тихая потеря данных.
Почему это работает
Почему не просто запустить миграцию в окне обслуживания? Для небольших таблиц — окно обслуживания вполне подходит. Для таблицы на 100 миллионов строк ALTER TABLE может держать эксклюзивную блокировку часами, пока переписывает таблицу. Современный Postgres поддерживает ALTER TABLE ... ADD COLUMN ... DEFAULT ... без переписывания (начиная с Postgres 11), но изменения типов колонок по-прежнему требуют переписывания. Инструменты онлайн-изменения схемы (pg_repack, pgroll, паттерны schema-change-as-migration) позволяют вносить ломающие изменения с нулевым даунтаймом за счёт операционной сложности. Expand-then-contract — кодовая версия той же дисциплины.
Пять типичных сбоев в продакшне
Эти сбои повторяются вне зависимости от тщательности проектирования схемы.
1. Неявное преобразование типов убивает индекс. Запрос WHERE user_id = '42' (строковый литерал вместо целого числа) вызывает неявный cast на каждой строке. Postgres не может использовать целочисленный B-tree индекс, потому что сравнение происходит между индексированным integer и text-значением. EXPLAIN ANALYZE показывает последовательный скан. Исправление: типизированные построители запросов или ORM, которые привязывают правильный тип параметра.
2. NULL-семантика в джойнах приводит к тихой потере строк. LEFT JOIN, где джойн-колонка содержит NULL, молча теряет эти строки в последующих inner JOIN. NULL != NULL (NULL не равен ничему, включая другой NULL). Паттерн зрелого разработчика: использовать IS DISTINCT FROM для null-безопасного сравнения и явный COALESCE для null-толерантной логики в WHERE.
3. Каскад на каскаде — неограниченные транзакции. DELETE на сильно-каскадированной корневой строке создаёт транзакцию на несколько миллионов строк, держащую блокировки минутами. Все записи в каскадированные таблицы блокируются. Исправление: soft-delete корневой строки (deleted_at = now()), затем пакетная очистка дочерних с явными DELETE-циклами по 10 000 строк.
4. Дрейф схемы через ad-hoc ALTER. Разработчик запускает ALTER TABLE напрямую в продакшне, минуя систему миграций. Инструмент миграций больше не соответствует реальности; будущие миграции загадочно ломаются или создают несогласованное состояние. Исправление: каждое изменение схемы проходит через систему миграций. CI применяет это правило.
5. JSONB-колонки, выросшие до реляционной формы. Колонка «metadata» типа JSONB накопила 30 типизированных полей за два года. Каждая новая фича добавляет IF metadata->>'feature_x' = .... Колонка теперь представляет schema-on-read без применения. Правильный шаг — извлечь колонки, но миграция дорогая и постоянно откладывается. Исправление: правило «promote-to-column»: как только поле появляется в WHERE, запланировать миграцию с извлечением колонки.
Команде нужно удалить workspace и все его данные (проекты, задачи, комментарии). Есть 5 миллионов комментариев, каскадированных из задач. Что не так с `DELETE FROM workspaces WHERE id = X` при CASCADE FK?
Колонку нужно переименовать с `user_name` на `display_name` в таблице на 50M строк без даунтайма. Выберите подход.
Запрос `WHERE user_id = '42'` (строковый литерал) на колонке, объявленной как BIGINT, выполняет последовательный скан по таблице на 30M строк, несмотря на B-tree индекс на user_id. Почему?
- 01Опишите паттерн expand-then-contract и объясните, почему он необходим при переименовании колонки в большой таблице с общей схемой.
- 02В чём конкретный риск DEFERRABLE INITIALLY DEFERRED constraints в сравнении с IMMEDIATE?
- 03Назовите три из пяти типичных сбоев в продакшне и дайте исправление для каждого.
Применение переходов состояний принадлежит триггерам или RLS, когда правило требует процедурной логики; чистый CHECK обрабатывает per-row инварианты. DEFERRABLE INITIALLY DEFERRED откладывает проверки FK до COMMIT — полезно для циклических зависимостей, опасно, потому что нарушения откатывают всю транзакцию. Версионирование схемы использует файлы миграций в системе контроля версий; ломающие изменения применяют expand-then-contract для избежания даунтайма. Пять типичных сбоев в продакшне (неявное преобразование типов убивает индексы, NULL-семантика в джойнах, неограниченные каскадные транзакции, дрейф схемы через ad-hoc ALTER, разрастание JSONB) — каждый имеет конкретное исправление, которое дешевле, чем обнаружить его в продакшне. Решения по схеме обладают инерцией — цену неправильного дизайна платят при миграции.
встречается в164
- Почему 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
- Что такое воркеры и зачем они нужныjunior
- Механика web workers: dedicated, shared и OffscreenCanvasmiddle
- Structured clone и transferablesmiddle
- Жизненный цикл service worker и стратегии кешированияmiddle
- SharedArrayBuffer, Atomics и cross-origin isolationsenior
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Пулы воркеров, Comlink и наблюдаемость в продакшене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
- Конверт IPjunior
- Читаем IP-заголовокmiddle
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Что делает TLS и зачем он нуженjunior
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- Расписание ключей, SNI, ALPN и расширенияsenior
- Защита 0-RTT, ECH, гибридный PQ и продакшн TLSsenior
- 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
- Двенадцать слоёв: один URL, семь действующих лицjunior
- 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
- Что такое OpenTelemetry: API, SDK, Collector, OTLPjunior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Что такое trace propagation и почему сломанная propagation хуже отсутствия трейсовjunior
- traceparent и tracestate: полный формат W3C-заголовкаmiddle
- Baggage и async-границы: перенос контекста через очереди и callback''''иmiddle
- Async context на разных языках, service mesh, миграция B3 и безопасностьsenior
- Production-сбои propagation, span links и платформенный дизайнsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- 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