Если при слове «нормализация» у тебя начинается зевота, а менеджер с порога предлагает «спроектировать базу» — этот текст для тебя.
В последнее время периодически испытываю некое чувство раздражения(немного режет слух), когда разработчики или даже менеджеры начинают говорить о проектирование схемы базы данных. Когда обсуждение любого проекта, любой разработки начинается именно с базы.
Бьет это предложение сразу по двум моим болям:
любовь к DDD и объектному моделированию. Наверное да, предложение: давайте спроектируем концептуальную или объектную модель, вместо давайте спроектируем схему базы будет звучать как‑то сложнее, запутаннее что ли.
после довольно обширного опыта работы с документо‑ориентированной базой данных, как‑то начинаешь иначе смотреть на вопросы нормализации и проектирования модели данных.
Зачем вообще писать пост про нормализацию/денормализацию?
1. Чтобы напомнить: в реальной жизни всё не так чёрно‑бело, как в учебнике
— Академическая нормализация — это прекрасно, но мир давно не делится на «правильное» и «неправильное».
— Иногда бизнесу, продукту или стартапу нужно «выжить сейчас», а не сделать идеальную схему.
2. Чтобы показать альтернативу для «нестандартных» сценариев
— Не все проекты одинаковы: где‑то нужны отчёты и консистентность, а где‑то важнее скорость, гибкость, «time‑to‑market» или write‑heavy сценарии.
— Денормализация (или document storage) — хороший инструмент для таких случаев.
3. Чтобы поделиться опытом «где не надо бояться отходить от догм»
— Многие разработчики по инерции «режут всё на 7 таблиц» даже там, где это только мешает.
— Пост — способ сказать: «Не всегда надо делать, как учили. Иногда дешевле и проще сделать по‑другому.»
4. Чтобы честно поговорить о компромиссах
— Денормализация — не «универсальная истина», а просто ещё одна грань выбора.
— Важно не слепо её отвергать или боготворить, а понимать когда она реально полезна, а когда — обернётся болью.
5. Чтобы учиться думать, а не просто «копировать паттерны»
— Хочется, чтобы читатель после поста начал анализировать свои задачи:
«Зачем я нормализую?»
«Где я могу сделать проще?»
«В какой момент отказ от нормализации станет для меня ловушкой?»
Классика и исторический контекст
В далеком 1970 году Эдгар Кодд опубликовал работу «A Relational Model of Data for Large Shared Data Banks», в которой впервые представил реляционную модель данных и формализовал ее в виде математической системы. Он же заложил основу и реляционной алгебры. Примерно в то же самое время разрабатывался язык SQL, который по сути представлял интерфейс к реляционной алгебре. В честь Кодда названа одна из нормальных форм Нормальная форма Бойса — Кодда (третья усиленная). Первые три нормальные формы тоже заслуга именно Эдгара Кодда, а позже идеи дорабатывались и другими учеными.
Понятие нормализация как процесс преобразования данных в нормальные формы так же ввел Кодд.
По сути это правила по которым организуются данные:
не должно быть лишнего дублирования
данные должны быть «чистыми» и непротиворечивыми
не было «глюков» при обновление и удаление
существует всего семь нормальных форма, обычно рассматриваются только три первые:
1NF — Первая нормальная форма
Скрытый текст
В таблице нет «таблиц внутри таблицы», каждое поле — «атомарное» (то есть одно значение, а не список, не массив, не вложенный объект).
Исходная ненормализованная таблица:
Сотрудник |
Номер телефона |
Иванов И. И. |
283-56-82 390-57-34 |
Петров П. П. |
708-62-34 |
В чём проблема?
В поле »Номер телефона» хранятся сразу несколько значений.
Чтобы найти или обработать отдельный телефон, нужно парсить строку.
Таблица, приведённая к 1НФ, являющаяся правильным представлением некоторого отношения:
Сотрудник |
Номер телефона |
Иванов И. И. |
283-56-82 |
Иванов И. И. |
390-57-34 |
Петров П. П. |
708-62-34 |
Что изменилось?
Каждое поле «Номер телефона» теперь содержит только одно значение.
Для одного сотрудника можно добавить сколько угодно номеров, не нарушая структуру.
Запросы на поиск/обновление отдельных номеров становятся простыми.
2NF — Вторая нормальная форма
Скрытый текст
1NF + Убираем частичные зависимости: чтобы каждое неключевое поле в таблице зависело от всего составного (или простого) первичного ключа, а не только от его части.
Составной ключ — это когда для уникальности строки нужны два и более столбца (например, Заказ + Товар).
Если есть поля, которые зависят только от одной части ключа, их надо вынести в отдельную таблицу.
Плохо (не соответствует 2NF):
Заказ |
Товар |
Адрес доставки |
123 |
Хлеб |
ул. Ленина, 5 |
123 |
Молоко |
ул. Ленина, 5 |
124 |
Сыр |
ул. Мира, 7 |
124 |
Яблоки |
ул. Мира, 7 |
Хорошо (соответствует 2NF):
Таблица «Заказы»
Заказ |
Адрес доставки |
123 |
ул. Ленина, 5 |
124 |
ул. Мира, 7 |
Таблица «Товары заказа»
Заказ |
Товар |
123 |
Хлеб |
123 |
Молоко |
124 |
Сыр |
124 |
Яблоки |
Что изменилось?
Нет дублирования, легче обновлять адрес, если он изменится.
Для чего это нужно?
Экономия места: не дублируется информация (например, адрес доставки).
Легче вносить изменения: если адрес изменился — правим одну строку.
Нет риска противоречий: нельзя случайно указать разные адреса для товаров одного заказа.
Вторая нормальная форма — чтобы все дополнительные (не ключевые) данные зависели от всей «идентификации» строки (всего ключа), а не только от её части.
3NF — Третья нормальная форма
Скрытый текст
Третья нормальная форма (3NF) — это правило, по которому:
В таблице нет «транзитивных зависимостей» между неключевыми полями.
Любое неключевое поле зависит только от ключа, а не от другого неключевого поля.
Проще говоря:
Каждое неключевое поле должно зависеть только от уникального идентификатора строки (ключа), а не «через» другие поля.
Плохо (не соответствует 3NF):
Пользователь |
Город |
Регион |
Вася |
Москва |
Московская |
Петя |
Казань |
Татарстан |
Оля |
Сочи |
Краснодарский |
В чём проблема?
«Регион» зависит не от «Пользователя», а от «Города» (т. е. транзитивная зависимость).
Если для Москвы поменяется регион — надо менять во всех строках, где «Москва».
Хорошо (соответствует 3NF):
Таблица «Пользователи»
Пользователь |
Город |
Вася |
Москва |
Петя |
Казань |
Оля |
Сочи |
Таблица «Города»
Город |
Регион |
Москва |
Московская |
Казань |
Татарстан |
Сочи |
Краснодарский |
Что изменилось?
Теперь «Регион» связан только с «Городом», а не с пользователем напрямую.
Если регион у города меняется, меняем только в одной строке таблицы «Города».
Нет дублирования, нет риска рассогласования.
Преимущества нормализации в том, что мы избавляемся от дублирования, упрощаем обновление данные и предотвращаем противоречия.
Но за нормализацию приходится платить:
1. Сложность и «размазывание» данных по таблицам
Даже для одной бизнес-сущности (например, «заказ») после 2NF–3NF получится 3–7 таблиц: заголовок, позиции, адреса, статусы, документы, справочники…
Это усложняет код, запросы, схему, миграции и понимание для разработчиков.
Любой запрос или даже простое отображение — всегда JOIN‑ы.
2. Производительность на чтение
Много join‑ов замедляет запросы.
Чем больше таблиц надо соединять — тем больше нагрузка на БД и тем дольше выполняются запросы, особенно на больших данных.Становится сложно делать быстрые выборки «всё про заказ целиком» — приходится собирать агрегат из кусочков.
3. Объектно-реляционный разрыв (ORM impedance mismatch) и сложные инструменты ORM
Объекты со сложной вложенностью плохо ложатся на реляционную модель данных. Чтобы хоть как-то с этим справиться, появились сложные ORM-инструменты, которые частично скрывают боль, но добавляютмагии, непрозрачности и собственных ограничений.
Чем сложнее доменная модель и чем больше таблиц — тем больше ты страдаешь от несовпадения между тем, «как оно в коде» и «как оно в базе».
Hibernate как главный представитель ORM в мире java имеет несколько классических «граблей»:
1. Проблема N+1 запросов
Суть проблемы:
Ты делаешь один запрос к основной сущности (например, список заказов), а потом для каждой из них Hibernate делает отдельный запрос к связанным сущностям (например, к товарам в каждом заказе).В чём боль: Ты думаешь, что сделал один «красивый» запрос — а реально в БД уходит десятки или сотни дополнительных, и всё тормозит.
2. Магия и непрозрачность
Hibernate автоматически генерирует SQL. Иногда запрос получается неоптимальным, сложно понять, что реально уходит в базу (особенно с Criteria API или сложными связями).
Оптимизация становится «развлечением для профи»: нужно смотреть логи, анализировать планы выполнения, писать «ручные» запросы.
3. Лишние/избыточные транзакции и блокировки
Hibernate любит всё оборачивать в транзакции — иногда это полезно, иногда создаёт лишние блокировки и проблемы с производительностью.
3. Entity Graph
Ещё одна «невидимая» проблема — это работа с entity graph через ORM. Когда модель становится сложной, с множеством связанных сущностей, разработчик легко теряет контроль над реальным количеством SQL-запросов.
Ты просто итерируешься по объектам, а на самом деле ORM в фоне выполняет десятки или сотни запросов, проходя по всему графу сущностей.
Часто об этом узнаешь только когда прод «ложится» под нагрузкой, а причина — неочевидная «магия» внутри ORM.
4. LazyInitializationException
Если лениво загружаемые поля используются вне сессии Hibernate (например, сериализация после закрытия транзакции), бросается ошибка.
5. Антипаттерн Hibernate: hbm2ddl.auto=update
Заманчиво на старте «поручить» Hibernate самому поддерживать схему БД, включив hbm2ddl.auto=update. Но это быстро превращается в ловушку: никакого контроля над миграциями, риски потери данных, неожиданные изменения схемы, отсутствие истории и невозможность отката.
Обычно данный подход считается антипаттерном. Вместо этого используйте Flyway или Liquibase — управляйте миграциями явно.
Кейс из практики: «160 таблиц для справочника»
Скрытый текст
Ситуация:
В компании мини-монолит, который синхронизирует данные с Битриксом и подтягивает справочные данные по компаниям/юрлицам из КонтурФокус. Сервис проверяет, находится ли физлицо или компания в глобальных санкционных списках.
В КонтурФокус приходит базовая инфа по ИНН/ОГРН — есть базовая сущность KonturBasic и ещё 18 «разветвлений», плюс куча связей, в итоге 21 репозиторий.
Всё нормализовано до предела, в итоге в базе… 160 таблиц!
Вопрос — зачем так? Почему не положить какую-то часть в JSON?
Данные справочные, не изменяются.
Структура жёстко связанная с API стороннего сервиса.
Вставка происходят только по новым данным, никаких изменений или аналитики внутри.
Запросы к этим данным обычно идут «по компании» (ИНН/ОГРН), нужен срез справочной информации целиком.
К чему приводит избыточная нормализация?
Взрыв количества таблиц и репозиториев — Трудно поддерживать, приходится поддерживать схемы, связи.
Усложнение запросов — Для того чтобы получить полный справочный «профиль» компании, надо тащить join-ы по десяткам таблиц или руками собирать агрегат.
Отсутствие реальной бизнес-ценности от нормализации — Никто не делает аналитику или массовые апдейты.
— Данные нужны «как есть», и поч ти всегда целиком.
Альтернатива SQL или NoSql
В 2000-х классические реляционные базы (Postgres, MySQL, Oracle) были стандартом для почти всех бизнес-приложений.
Но с ростом интернета и web-сервисов, взрывом Big Data, мобильных приложений и появлением стартап-культуры появились новые задачи:
Очень большие объёмы данных (терабайты+), часто write-heavy (логи, клики, ивенты, telemetry).
Быстро меняющаяся структура данных — MVP, прототипы, гибкие продукты.
Высокая нагрузка на запись/чтение и требование легко горизонтального масштабироваться (на много серверов).
NoSQL (Not Only SQL) — это целый класс новых баз данных, которые отказались от жёсткой схемы и «классического» SQL‑стиля хранения данных.
Идея:
Не заставлять всех хранить данные только в виде строго нормализованных таблиц.
Позволить хранить данные в виде документов, графов, пар ключ-значение, столбцов.
Дать возможность менять структуру данных «на лету».
Обеспечить лёгкое горизонтальное масштабирование (шардирование).
MongoDB
MongoDB создана компанией 10gen (позже MongoDB Inc.) в 2007–2009 гг.
Изначальная задача — база для новых web-приложений, стартапов, SaaS, где схема данных меняется часто, а скорость и простота важнее классической структуры.
Документо-ориентированная модель:
Каждый объект хранится как JSON-документ (на самом деле BSON, но это не принципиально).
Нет необходимости проектировать сложные схемы и миграции на старте.
Можно просто добавлять новые поля, менять структуру объектов, не ломая старые данные.
Преимущества
Документо ориентированная — база может хранить не кортежи записей, а JSON‑документы. Что в какой‑то степени может побороть объектно‑реляционный разрыв возникающий в реляционных базах. Все‑таки объект с его сложной вложенностью это не совсем тоже самое, что плоская таблица. ORM как раз в какой‑то степени и призваны решить данную проблему. Но в монге, представление объекта в виде JSON‑документа выглядит куда более естественным образом. И в чем‑то эта база будет проще для разработчиков чем реляционная, но после более классической СУБД свой способ мышления, конечно, перестроить может быть уже сложно.
Гибкость — в монго нет строгой схемы данных. По сути в одной коллекции одновременно могут храниться две версии одного и того же объекта, одна с одним набором полей, другая с другим и это абсолютно не будет проблемой для базы данных. Таким образом не требуется писать дополнительных миграций. Помимо миграций может быть вообще так, что данные по своей природе сильно гетерогенные, но тем не менее обрабатывать их и хранить необходимо. При разработке MVP также считается, что монга может уменьшить Time to market за счет своей гибкости.
Масштабируемость — хорошая масштабируемость вытекает из простоты связей. В отличие от реляционной базы данных в Mongo не принято раскладывать объекты по разным таблицам, там объект храниться как единый агрегат в своей коллекции. В монго нет внешних ключей, которые бы обеспечивали ссылочную целостность, за этим разработчик должен следить самостоятельно. Взамен такие упрощения дают отличное горизонтальное масштабирование — шардирование коллекций. Соответственно база способна выдерживать большую нагрузку на запись.
Недостатки
слабый контроль целостности — нет Foreign Key, логика управления связями переносится в приложение
Joinы представлены операцией $lookup, работает как правильно медленнее join ов в постгрес
отсутствие жесткой схем — как плюс в каких‑то случаях, так и минус. Тем не менее в приложениях схема данных все равно предполагается и эволяция схемы с миграциями тоже присутствует. И если вы столкнетесь с необходимостью обновления каких‑либо новых полей с дефолтными значениями, то придется обновить документы во всей коллекции вместо модификации схемы в реляционной базе.
Транзакции между коллекциями медленные. Транзакции появились начиная с 4й версии монги, но они как правило медленнее, чем в постгрес. при использование транзакций теряется скорость на запись
Шардирование требует навыков от админов. операции с переносом шардов довольно сложные.
Поддержка и инструменты — меньше специалистов и инструментов чем в экосистеме postgres
Представления, хранимые процедуры, триггеры — всего этого тоже нет
В защиту монго хочется сказать, что у меня был опыт работы с проектами, где монго использовалась повсеместно. Основной бизнес процесс строился вокруг лишь одного агрегата, который был неизменяемым, менялся только статус. Плюс был довольно большой поток на запись данных сущностей и тут монга была в целом оправдана.
DDD и агрегаты
Агрегат — это группа связанных сущностей или объектов‑значений. Чей жизненный цикл рассматривается как единый. т. е. вложенная сущность не может существовать без корневого агрегата. Агрегат гарантирует соблюдение бизнес инвариантов и обеспечивает транзакционную целостность.
Простой пример заказ Order и его позиции OrderItem. Очевидно, что позиция заказа не должна существовать без самого заказа и если удаляется заказ, то должна удаляться и позиция. В реляционной базе это можно автоматизировать как раз через каскадное удаление и контроль ссылок. В монго же эти позиции могут содержаться сразу в документе вместе с заказом. Сложности могут начинаться когда к примеру потребуется аналитика по проданным товарам особенно со сложными джоинами, такая операция в постргесе была бы сильно проще и быстрее чем в монго с денормализацией.
class Order { // aggregate root
List<OrderItem> items; // value objects
Address deliveryAddress;
void addItem(...) { ... } // изменение только через Order
}
Важные свойства агрегата:
Он всегда имеет корень (aggregate root), через который идёт взаимодействие.
Внутри агрегата может быть несколько сущностей и объектов-значений (Value Objects), но снаружи агрегат воспринимается как единое целое.
Агрегаты обычно изменяются целиком и неразрывно (атомарно).
Именно агрегаты составляют основу доменной модели, определяют границы целостности данных и упрощают бизнес-логику.
Почему агрегаты и MongoDB — идеальная пара?
Классические реляционные базы вынуждают разбивать агрегаты на десятки таблиц, раскладывать по foreign key, а потом соединять обратно через join'ы. Это как раз и создаёт знаменитый объектно-реляционный разрыв: ты хочешь хранить единый объект (агрегат), а вынужден «раскладывать» его по кусочкам и постоянно «собирать» обратно.
Документо-ориентированные базы, такие как MongoDB, позволяют хранить агрегаты так, как они существуют в доменной модели — единым документом:
Один агрегат — один документ.
Никаких join-ов: агрегат всегда берётся целиком и сохраняется целиком.
Нет object-relational mismatch: структура агрегата в коде и в базе практически совпадает, нет лишнего преобразования и лишней ORM-магии.
Легко поддерживать целостность агрегата: вся бизнес-логика агрегата — в одном документе.
Пример:
{
"orderId": "123",
"customer": {
"name": "Иван Иванов",
"address": "ул. Ленина, 10"
},
"items": [
{"name": "Хлеб", "price": 50},
{"name": "Молоко", "price": 80}
],
"total": 130
}
Это и есть агрегат целиком: сохраняется и извлекается одной операцией.
MongoDB позволяет хранить агрегаты «как есть», без боли и лишних манипуляций.
Однако стоит помнить, что такой подход подходит, если:
агрегаты не требуют сложной аналитики по отдельным полям;
изменения агрегата почти всегда атомарны и целостны;
не требуется сложная консистентность между агрегатами.
Если условия выполнены — связка MongoDB + агрегаты DDD становится практически идеальным решением.
Кейс 2: Сервис фискализации платежей (MongoDB)
Скрытый текст
Архитектура:
— Приложение состояло из более чем 30 микросервисов, каждый из которых использовал MongoDB как основное хранилище.
— Поток: входящие платежи → формирование чеков → длинный pipeline микросервисов → фискализация → отправка чека в ОФД.
Характер данных:
— Чеки — неизменяемые («иммутабельные» агрегаты).
— Большая нагрузка на запись (чеков очень много), чтение — заметно реже.
— Запросы на изменение данных практически отсутствуют (только хранение и извлечение).
Реализация:
— Никакой магии ORM — все репозитории писались вручную, что исключало «невидимые» проблемы вроде N+1-запросов или неожиданного lazy‑loading.
— Каждый запрос был написан под конкретную задачу, в том числе — агрегации, «ручные join„ы“ (через $lookup), статистика по чекам.
— Особой боли не возникало. С MongoDB жизнь вполне возможна, если не упарываться в аналитику и BI.
Статистика и отчёты:
— Для серьёзной статистики и аналитики была заведена отдельная реляционная база (Postgres), куда данные реплицировались или агрегировались.
— Если бизнес «вдруг» требовал сложный отчёт «на вчера» — строить его было не всегда быстро, но такие запросы и правда возникали нечасто.
Гибкость схемы на практике:
— Хоть Mongo и «схемалесс», по факту структура чека была практически фиксирована (задана законодательно).
— За годы работы поля менялись крайне редко, и любые изменения всегда были с обратной совместимостью (новые поля были опциональны, старая и новая схемы уживались спокойно).
Модно-молодежно Polyglot Persistence
Ну по правде сказать не сильно то и молодежно, Мартин Фаулер еще в далеком 2011 году писал на эту тему https://martinfowler.com/bliki/PolyglotPersistence.html
Вот какой пример приводит Фаулер:
Недавно я общался с командой, чьё приложение, по сути, создавало и обслуживало веб-страницы. Они искали элементы страниц только по идентификатору, им не требовались транзакции и не было необходимости делиться своей базой данных. Подобная задача гораздо лучше подходит для хранилища «ключ-значение», чем для корпоративного реляционного молотка, который им приходилось использовать. Хорошим примером использования NoSQL в этой задаче является The Guardian, где отметили определённый прирост производительности при использовании MongoDB по сравнению с их предыдущим реляционным вариантом.
Недавно я общался с командой, чьё приложение, по сути, создавало и обслуживало веб-страницы. Они искали элементы страниц только по идентификатору, им не требовались транзакции и не было необходимости делиться своей базой данных. Подобная задача гораздо лучше подходит для хранилища «ключ-значение», чем для корпоративного реляционного молотка, который им приходилось использовать. Хорошим примером использования NoSQL в этой задаче является The Guardian, где отметили определённый прирост производительности при использовании MongoDB по сравнению с их предыдущим реляционным вариантом.
Polyglot Persistence (полиглот-персистентность) буквально означает «многоязычное хранение».
Вместо того, чтобы использовать одну универсальную СУБД (например, только Postgres или только Mongo), проект может комбинировать несколько различных типов баз данных, каждая из которых оптимально подходит под конкретную задачу:
Для аналитики и отчетов — реляционные базы (например, Postgres).
Для агрегатов и событий — документо-ориентированные базы (например, MongoDB).
Для кеша и быстрого доступа — in-memory решения (например, Redis).
Для аналитики и временных рядов — time-series базы (например, ClickHouse).
Для поиска — специализированные решения (ElasticSearch).
Но у всего есть своя цена и разумеется развернуть два кластера например с MongoDB и Postgres в прямом смысле стоит денег, а еще и дополнительных затрат на администрирование. Разумеется без острой необходимости никто не станет так существенно усложнять инфраструктуру.
Поэтому реальный Polyglot Persistence — это не «по приколу», а всегда очень осознанный компромисс.
Без явной выгоды (по скорости, удобству, поддерживаемости, архитектурному разнесению) никто не будет плодить зоопарк баз просто «для тренда».
Архитектор обязан учитывать стоимость инфраструктуры и сопровождения — иначе выигрыш на бумаге быстро превращается в лишние затраты в жизни.
Компромисс Postgres JSONB
Честно — мне нравится MongoDB. Это инструмент, который реально ускоряет старт, даёт свободу, снижает головную боль с миграциями и «навязыванием схемы». Но с опытом всё же признаёшь: Mongo — это не «универсальная таблетка», а скорее специнструмент для write‑heavy задач, быстрого прототипирования, или хранения агрегатов без сложных связей и аналитики.
При этом в большинстве «рядовых» проектов бизнес‑логика проще:
Нет лавинообразных изменений в структуре,
Требуется стабильность и прозрачность схемы,
Отчёты и аналитика — неотъемлемая часть развития продукта,
И команда привыкла к SQL, нормальным транзакциям и «человеческому» мониторингу.
Именно тут идеально заходит компромиссный вариант: Postgres + JSONB.
Где важна реляционная структура, связи, валидация — используем классические таблицы, как положено.
Где нужно быстро хранить агрегаты, которые редко меняются или не требуют сложной аналитики — используем jsonb‑поля.
Нет нужды пилить отдельную Mongo, усложнять инфраструктуру и сопровождение, но и нет нужды «насильно» раскладывать каждый агрегат на 7 связанных таблиц.
Это и есть «лучшее из двух миров»:
Полная поддержка SQL, транзакций, индексов, сложных join‑ов — для всего, где это нужно,
Гибкость и скорость изменений — для нестабильных или экспериментальных данных.
Причём, если проект со временем «взрослеет» и появляется потребность в аналитике по части данных из jsonb — их всегда можно мигрировать в нормализованную структуру без миграции на другую СУБД.
Если коротко:
«Если нормализация кажется избыточной, а запускать отдельную MongoDB ради пары нестабильных агрегатов — слишком жирно, просто используйте JSONB в привычном Postgres. Это честный компромисс между гибкостью и надёжностью, позволяющий команде двигаться быстро, не теряя контроля и поддержки.»
«Да, я тоже думал, что с jsonb можно всё. Но когда пытался сделать отчёт за год — впервые начал гуглить „как мигрировать обратно в нормализованную схему“.»
Кейс 3. Стартап растёт: Postgres, микросервисы и денормализация на этапе неопределённости
Скрытый текст
Контекст:
— Новый проект, который начинал как MVP, но начал быстро расти и двигаться к полноценному продукту.
— Был взят курс на архитектурное развитие: выделение ключевых сервисов, постепенный уход от монолита к микросервисам.
— Каждый сервис был «узким» по модели: 1–2 агрегата, простые связи.
Особенности разработки:
— Нагрузка на сервисы и данные была невысокой, никакого highload или миллионов пользователей.
— Бизнес‑процессы менялись буквально «на ходу»: кейсы не были полностью проработаны, многие требования появлялись по мере внедрения.
— Запросы к данным «рождаются» по мере роста, было много неопределённости (типично для быстрорастущего стартапа).
Техническое решение:
— В качестве основной базы выбран Postgres.
— Для большинства кейсов использовалась денормализация через JSONB: агрегаты хранились целиком, структура могла плавать, новые поля добавлялись «на лету» без долгих миграций.
— Для аналитики, отчётности и BI‑отчётов использовалась отдельная read‑model, которую можно было подстраивать и дорабатывать без риска сломать «боевые» данные.
— При этом основной упор делался на гибкость схемы: если бизнес или требования к данным радикально изменятся — можно будет безболезненно мигрировать на новую структуру, т.к. объём данных был небольшой.
Зачем так?
— Не было смысла городить «идеальную» нормализованную схему на этапе высокой неопределённости: рисковали бы потратить уйму времени на поддержку и миграции схемы, которые быстро устаревают по мере развития бизнеса.
— Важно было быстро реагировать на новые требования, не тратя время на сложные миграции и «лишние» связи.
Как итог:
— Денормализация через JSONB в Postgres дала нужную гибкость, позволила экономить время на схемах и миграциях, при этом всё равно была возможность в будущем «разложить» агрегаты на нормализованные таблицы, если бизнес устаканится и объём данных возрастёт.
Выводы
Нормализация — не серебряная пуля, но и не «враг»
С одной стороны, нормализация — не идеал и не универсальное решение.
Да, бывают ситуации, когда денормализация или хранение агрегата целиком («как есть», без дробления по таблицам) сильно упрощает жизнь: меньше join‑ов, проще код, быстрее MVP, меньше миграций.
Но!
Как только ты уходишь от нормализации — ты берёшь на себя ответственность думать на два шага вперёд:
Какие будут запросы на обновление?
Сможешь ли ты за разумное время и без ошибок обновить нужные куски данных, если они «спрятаны» в агрегате?Какие отчёты понадобятся через год?
Не окажется ли, что «классный документ» теперь надо расковырять на части ради аналитики?Что будет меняться по бизнесу?
Не появятся ли новые связи, которые будет очень сложно прикрутить «на ходу» к уже денормализованной структуре?Как ты будешь мигрировать данные, если формат поменяется?
Как будешь обеспечивать целостность?
Всё это — набор «подводных камней», которые в учебниках почти не обсуждаются, а в жизни встречаются часто.
Зрелость — это понимать эти риски и честно на них смотреть.
Почему нормализация всё ещё работает “по умолчанию”
Потому что, если просто всё нормализовать (до вменяемого уровня, не в абсурд), то:
Ты всегда сможешь написать любой запрос, любой отчёт, любой апдейт.
Схема будет предсказуемой, миграции — понятными, контроль целостности — автоматическим.
В 90% задач (и, кстати, в большинстве зрелых бизнес‑приложений) это реально работает, не создавая «лишних» проблем.
И вот в этом главный компромисс:
Ты можешь не нормализовывать всё подряд,
но обязан потратить время и «мозг» на то, чтобы предусмотреть последствия.
Лёгкость на старте оборачивается сложностью на поддержке — если не подумать заранее.
Если коротко
Нормализация — не серебряная пуля, но и не просто «старомодная догма».
Можно не нормализовывать — если ты чётко понимаешь, какие операции тебе реально понадобятся, и готов заложить время на поддержку, миграции и возможные будущие изменения.
Если же хочется «просто, надёжно и с минимальным риском» — нормализация остаётся самым универсальным и безопасным путём.
P. S.
Если вам близки темы архитектуры, нормализации и реальные кейсы вроде «160 таблиц против одного jsonb» — буду рад обсудить в моём Telegram-канале.
CTO / архитектор для старта или перезапуска проекта — пишите: @korobovn, korobovn@gmail.com
Комментарии (0)
gerashenko
19.09.2025 17:11Как-то много всего. На входе говорили про быстрый старт, отсюда простой вопрос, может ли Mongodb все то, что может Postgresql, и, соответственно, наоборот. Поэтому выбирается Postgresql, вместо двух БД или той БД, которая не сможет покрыть отчёты, при необходимости. JSONB - тема, там где нет ясности по структуре. Mongodb - наверно больше оптимизация, но в таких случаях, вроде, все больше берут cassandra или scylla, есть ощущение, что Mongodb берут все реже.
Hamletghost
19.09.2025 17:11Нет примерно никаких ситуаций, чтобы использование Mongo было оправдано. Начиная с лицензии и заканчивая ее техническими проблемами (тормоза, неэффективная и хрупкая репликация)
upcFrost
19.09.2025 17:11Все б хорошо если б монга не была такой тормозной и неэффективной по памяти/хранилищу
Tzimie
Забавно: я database performance guru (MSSQL). После известных событий 2022 года искал новую работу. Техническое интервью. Первый вопрос: какие нормальные формы вы знаете?
Я много проводил нормализаций и денормализаций, но честно признался что ни разу в жизни названия этих форм мне и не пригодились
OlegZH
Смысл, наверное, не в названиях. А в существе дела. Существо-то Вы, наверное, не забывали никогда.