Базы данных
Нормальные формы, денормализация и почему схемы «прилипают»
Младший инженер хранит город клиента в каждой строке заказа — «удобно для отчётов». Через два года клиент переезжает. Теперь половина исторических заказов показывает старый город, половина — новый. Один и тот же факт живёт в двух местах и они расходятся. Нормализация — дисциплина, которая предотвращает это.
Нормальные формы простыми словами
Нормализация — серия прогрессивно строгих правил разбиения таблиц, чтобы ни один факт не хранился дважды.
1NF — атомарные ячейки. Каждая ячейка должна содержать одно значение. Никаких списков через запятую внутри колонки, никаких повторяющихся групп, никаких массивов под видом колонок (tag1, tag2, tag3). Если ячейку можно распарсить через split(','), таблица нарушает 1NF.
2NF — полная ключевая зависимость. Каждая неключевая колонка должна зависеть от всего primary key, а не от его части. Имеет значение только для составных primary keys. Пример нарушения: таблица order_items(order_id, product_id, product_name) — product_name зависит только от product_id, а не от составного ключа. Решение: перенести product_name в таблицу products.
3NF — нет транзитивных зависимостей. Каждая неключевая колонка должна зависеть только от primary key, а не от другой неключевой колонки. Пример нарушения: orders(order_id, customer_id, customer_city) — customer_city зависит от customer_id, а не от order_id. Решение: перенести customer_city в таблицу customers (или addresses).
BCNF — каждый детерминант является candidate key. Строже 3NF; имеет значение когда таблица имеет несколько перекрывающихся candidate keys. Редко в продакшне; часто в академических примерах. В практике цельтесь в 3NF; переходите к BCNF только когда можете назвать конкретный сценарий перекрывающихся candidate keys.
| Нормальная форма | Правило | Форма нарушения | Решение |
|---|---|---|---|
| 1NF | Атомарные ячейки, нет повторяющихся групп | Список через запятую в одной колонке | Отдельная таблица или типизированный массив |
| 2NF | Полная ключевая зависимость | Неключевая колонка зависит от части составного PK | Перенести в таблицу-владелец |
| 3NF | Нет транзитивных зависимостей | Неключевая колонка зависит от другой неключевой | Вынести в отдельную таблицу |
| BCNF | Каждый детерминант — candidate key | Несколько перекрывающихся candidate keys | Декомпозиция; редко на практике |
Суть — не в следовании правилам, а в устранении сценария, когда две строки расходятся об одном факте. Если можно обновить email Анны в трёх таблицах, две из них будут устаревшими через год.
Денормализация: когда и зачем
Нормализация оптимизирует корректность при записи. При чтении она может быть дорогостоящей, потому что ответ на вопрос требует соединения многих таблиц. Денормализация намеренно дублирует данные для избежания JOIN на горячих путях чтения.
Сохранённые агрегаты. Колонка order_total_cents в orders, кешируещая SUM(order_items.unit_price_cents * quantity). Обновляется триггером или кодом приложения при каждой записи позиции. Отчётность обращается к одной колонке вместо агрегирования миллионов строк.
Широкие read-модели. Таблица user_profile, объединяющая имя, email, текущий адрес, количество последних заказов — пополняемая триггерами или кодом из нормализованных источников. Запросы дашборда обращаются к одной строке.
Материализованные пути. Хранение полного пути предков узла дерева как текстовой колонки (/root/section/subsection/), чтобы избежать рекурсивных JOIN при чтении иерархий. Классический паттерн для деревьев комментариев или оргструктур.
Дисциплина: денормализуйте на стороне чтения, сохраняйте авторитетные факты нормализованными, и признайте, что дубликаты должны согласовываться чем-то: триггером, плановым заданием, кодом приложения. Сценарий отказа — денормализация везде без плана согласования. Каждая колонка в итоге расходится с каждой другой.
Почему это работает
Почему 3NF, а не 4NF, 5NF или DKNF? Нормальные формы выше 3NF адресуют многозначные зависимости и зависимости соединений, которые встречаются в крайне академических схемах. Реальные продакшн-таблицы почти никогда не имеют таких форм. 3NF — точка убывающей отдачи: за ней вы разбиваете таблицы, которые не нужно разбивать, добавляете JOIN, которые не улучшают корректность, и замедляете чтение для схем, у которых не было проблем с integrity.
ORM-ловушки, незаметно ломающие дисциплину схемы
ORM абстрагируют SQL, но могут вносить ошибки на уровне схемы.
N+1 запросы. ORM загружает список 100 заказов, затем для каждого заказа отдельно получает клиента. Результат: 101 запрос там, где справился бы 1 JOIN. Во многих ORM это поведение по умолчанию, если не объявить eager-load (include, with, select_related). Импликация для схемы: каждая FK-связь должна быть аннотирована правильной стратегией загрузки по умолчанию; оставить это на неявную lazy-load — значит накапливать невидимые N+1.
Неявная генерация схемы. Многие ORM могут генерировать схему из определения модели. Сгенерированная схема часто не содержит: CHECK ограничений, составных индексов, индекса на FK-колонке, именованных ограничений. Результат — синтаксически корректная схема, нарушающая дисциплину этого урока. Воспринимайте ORM-сгенерированные схемы как отправную точку, а не финальный артефакт; добавляйте ограничения и индексы вручную или через файлы миграций.
Отсутствие обновления updated_at. ORM часто автоматически управляют created_at, но не updated_at. Отсутствие updated_at делает инкрементальную репликацию и аудит невозможными. Принудительно соблюдайте через триггер (BEFORE UPDATE ... SET updated_at = now()) или lifecycle hook ORM; никогда не доверяйте приложению помнить об этом.
Соглашения об именовании, которые окупаются
Они кажутся педантичными; они окупаются в каждом дашборде, каждом скрипте миграции, каждом кросс-командном запросе.
- Множественное число для таблиц (
users,orders,order_items) — единственное число тоже допустимо; выберите один вариант и соблюдайте везде. - snake_case — Postgres приводит неэкранированные идентификаторы к нижнему регистру; snake_case — продакшн-дефолт.
idкак имя primary key в каждой таблице; foreign keys называются<referenced_table_singular>_id(например,user_id,order_id).created_at,updated_at,deleted_atдля временных колонок.deleted_at IS NOT NULL— паттерн мягкого удаления.- Булевы колонки с префиксом
is_,has_,can_или как понятное свойство (active, неflag). - Именование CHECK ограничений — именуйте каждое:
CONSTRAINT chk_orders_status CHECK (status IN (...)). Безымянные ограничения выдают непонятные сообщения об ошибках.
Зрелые платформы CI-линтят эти соглашения, чтобы они соблюдались командами, а не запоминались отдельными людьми.
Упорядочьте эти действия по эволюции схемы от самых дешёвых к самым дорогим:
- 1 Добавить CHECK ограничение на существующую колонку
- 2 Добавить UNIQUE индекс (CREATE INDEX CONCURRENTLY)
- 3 Добавить FOREIGN KEY (требует прохода проверки по существующим строкам)
- 4 Изменить тип колонки (TEXT → INTEGER) — перезапись таблицы
- 5 Разбить JSONB-колонку на типизированные колонки — миграция с backfill
- 6 Разбить одну таблицу на две с FK — окно двойной записи, проверка паритета
- 7 Изменить primary key — каскадирует через каждый FK и индекс; почти никогда не делается
Таблица хранит `orders(order_id, customer_id, customer_city)`. Какую нормальную форму она нарушает и почему?
Дашборд отчётности нуждается в общей выручке по workspace, пересчитываемой каждые 5 минут. Выберите реализацию.
ORM-сгенерированная схема не имеет именованных CHECK ограничений и индексов на FK-колонках. Какие два конкретных сценария отказа это вызывает в продакшне?
- 01Объясните своими словами, почему 3NF является продакшн-дефолтной нормальной формой, и когда команды обоснованно идут выше (BCNF) или ниже (намеренная денормализация).
- 02Назовите три конкретных ORM-ловушки, незаметно нарушающих дисциплину схемы, и исправление для каждой.
- 03Что такое паттерн материализованного пути и когда он лучше рекурсивных CTE для запросов к деревьям?
Нормальные формы устраняют избыточные факты: 1NF (атомарные ячейки), 2NF (полная ключевая зависимость), 3NF (нет транзитивных зависимостей), BCNF (каждый детерминант — candidate key). Цельтесь в 3NF для продакшн-схем. Денормализуйте намеренно — сохранённые агрегаты, широкие read-модели, материализованные пути — с явным согласованием; никогда без плана синхронизации копий. ORM-сгенерированные схемы пропускают ограничения и индексы; добавляйте их. Соглашения об именовании (snake_case, множественное число, id PK, created_at/updated_at/deleted_at, именованные CHECK) соблюдаются через CI. Решения о схеме «прилипают»; цена неверной нормальной формы выплачивается при миграции, а не при проектировании.
встречается в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