Базы данных: SQL/NoSQL, моделирование и миграции
Как базы данных продолжают темы HTTP, REST и структуры проекта
В прошлых статьях вы построили основу backend-разработки:
HTTP задаёт транспорт и правила общения
REST фиксирует внешний контракт API
структура проекта и паттерны (контроллеры, use case, репозитории) помогают держать код поддерживаемымБаза данных добавляет следующую обязательную часть продакшена: надёжное хранение и извлечение данных. Именно на уровне базы данных обычно проявляются ключевые требования к системе: целостность, производительность, масштабирование, возможность безопасно выкатывать изменения.
!Где база данных находится в архитектуре backend-приложения
Что такое база данных в практическом смысле
База данных (БД) это система, которая:
хранит данные между перезапусками приложения
даёт язык запросов или API для чтения и записи
обеспечивает механизмы целостности (например, запретить дублирующийся email)
поддерживает конкурентный доступ (много пользователей одновременно)Для backend-разработчика база данных это не только “где лежат данные”, но и набор гарантий, ограничений и компромиссов.
SQL: реляционные базы данных
Реляционная модель простыми словами
В SQL-БД данные обычно представлены как:
таблицы
строки (записи)
столбцы (поля)Ключевая идея реляционных БД: вы явно описываете схему данных и связи между сущностями.
Популярные SQL-БД:
PostgreSQL Documentation
MySQL DocumentationПервичный ключ и внешние ключи
Первичный ключ (primary key) однозначно идентифицирует строку в таблице (например, users.id).
Внешний ключ (foreign key) связывает одну таблицу с другой и помогает сохранять целостность (например, orders.user_id должен ссылаться на существующего пользователя).Эти ограничения полезны тем, что ловят ошибки на уровне данных, даже если баг попал в код.
Транзакции и ACID
Транзакция это группа операций, которая выполняется как единое целое.
Реляционные БД обычно дают набор гарантий ACID:
Atomicity: либо выполнится всё, либо не выполнится ничего
Consistency: данные не переходят в запрещённое состояние (с учётом правил и ограничений)
Isolation: параллельные операции не “ломают” друг друга
Durability: после подтверждения (commit) данные не потеряются при сбоеПрактический пример, где транзакция обязательна: создание заказа и списание товара со склада. Если заказ создали, но склад не обновили из-за ошибки, система будет некорректной.
Дополнительное чтение по транзакциям в PostgreSQL:
PostgreSQL: TransactionsИндексы: почему запросы ускоряются и чем вы платите
Индекс это дополнительная структура данных, которая помогает быстрее находить строки по условию.
Типичный эффект:
чтение по индексируемым полям ускоряется
записи и обновления могут замедлиться, потому что индекс тоже нужно поддерживать
растёт потребление дискаГлавная практическая мысль: индекс создаётся не “на всякий случай”, а под конкретные запросы.
Ограничения и уникальность
SQL-БД позволяют задавать правила на уровне схемы:
NOT NULL запрещает отсутствующее значение
UNIQUE запрещает дубли (например, один email на одного пользователя)
CHECK ограничивает допустимые значенияТакие ограничения дополняют валидацию на уровне HTTP и use case: даже если вы забыли проверку в коде, база данных может защитить целостность.
NoSQL: нереляционные базы данных
NoSQL это не “против SQL”, а набор подходов, где вы обычно жертвуете частью удобств реляционной модели ради других преимуществ: гибкой структуры, горизонтального масштабирования, высокой скорости записи, специфичных моделей данных.
Важно: NoSQL решения очень разные по устройству, поэтому выбирать их нужно по типу данных и запросов.
Документные базы данных
Данные хранятся документами (часто JSON-подобного вида), структура может эволюционировать легче, чем фиксированная схема таблиц.
MongoDB ManualТипичный кейс: сущности со сложной вложенной структурой, где удобно хранить объект целиком и редко нужны сложные join.
Key-value хранилища
Работают как “словарь” ключ -> значение.
Redis DocumentationТипичный кейс: кэш, сессии, счётчики, rate limiting.
Wide-column базы данных
Оптимизированы под большие объёмы данных и масштабирование по узлам кластера.
Apache Cassandra DocumentationТипичный кейс: события, метрики, большие потоки записи, запросы по заранее продуманным ключам.
Графовые базы данных
Оптимизированы под запросы связей и обход графа.
Neo4j DocumentationТипичный кейс: социальные связи, рекомендации, “друзья друзей”, зависимости.
Согласованность и компромиссы
В распределённых системах часто возникает компромисс между:
согласованностью: все клиенты видят одно и то же в один и тот же момент
доступностью: система отвечает даже при проблемах с частью узлов
устойчивостью к разделению сети: система продолжает работать, даже если сеть “разрезало” на частиЭта идея известна как CAP-теорема.
Практический вывод для backend-разработчика:
SQL-БД чаще дают сильную согласованность и удобные транзакции в рамках одного кластера.
Некоторые NoSQL системы проще масштабируются горизонтально, но могут дать eventual consistency (согласованность “со временем”).
Выбор хранилища напрямую зависит от требований продукта: что важнее, строгая целостность или доступность под высокой нагрузкой.Моделирование данных: как превратить требования в структуру хранения
Начинать нужно с предметной области
Моделирование данных начинается не с таблиц, а с ответов на вопросы:
какие сущности существуют (пользователь, заказ, товар)
какие у них атрибуты (email, статус, цена)
какие между ними связи (у пользователя много заказов)
какие операции и запросы будут самыми частымиЕсли вы проектируете только “как красиво”, но не учитываете запросы, вы получите либо медленную систему, либо сложную схему, которую трудно развивать.
Связи между сущностями
Самые распространённые типы связей:
один к одному: пользователь и профиль (если профиль выделен отдельно)
один ко многим: пользователь и заказы
многие ко многим: товары и категории!Пример модели данных и связей для типового backend-сервиса
Нормализация и денормализация
Нормализация это подход, при котором данные раскладываются по таблицам так, чтобы:
уменьшить дублирование
избежать аномалий обновления (когда одно и то же значение нужно менять в нескольких местах)Пример проблемы без нормализации: email пользователя продублирован в таблице заказов. Если пользователь сменил email, нужно обновить много строк, и легко получить несогласованность.
Денормализация это осознанное добавление дублирования ради производительности чтения.
Правило продакшена: денормализация делается тогда, когда вы понимаете, какой запрос узкий горлышко, и чем вы платите за ускорение (сложность обновлений и риск рассинхронизации).
Проектирование под запросы
Backend редко работает с “всеми данными”. Обычно есть несколько ключевых запросов:
получить пользователя по email
получить список заказов пользователя с пагинацией
получить карточку заказа с позициямиИз этого следуют практические решения:
Индексы ставятся под реальные фильтры и сортировки.
Связи проектируются так, чтобы типовые сценарии работали без “каскада” запросов.
В API для списков почти всегда нужна пагинация, а в БД для неё нужен понятный порядок (например, сортировка по времени создания).Миграции: как безопасно менять схему базы данных
Что такое миграции
Миграция это версионированное изменение схемы базы данных: создать таблицу, добавить колонку, создать индекс, изменить ограничение.
В продакшене схема живёт и меняется вместе с кодом, поэтому миграции должны быть частью процесса разработки.
Почему нельзя “просто поправить базу руками”
Ручные изменения почти всегда приводят к проблемам:
невозможно повторить окружение (у разработчиков и на staging разные схемы)
непонятно, какая версия схемы сейчас в продакшене
откаты становятся рискованными
CI не может поднять базу “с нуля” на чистой схемеПоэтому миграции обычно хранятся в репозитории рядом с кодом.
Типовой жизненный цикл миграции
Разработчик создаёт миграцию как отдельный файл.
Миграция применяется локально и в тестах.
CI поднимает базу с нуля и прогоняет все миграции.
На деплое миграция применяется перед запуском новой версии приложения или как отдельный шаг пайплайна.Backward compatible изменения и подход expand/contract
Самая частая причина инцидентов: новая версия приложения ожидает новую схему, а часть серверов ещё работает со старой (или наоборот).
Чтобы выкатывать изменения без простоя, используется подход expand/contract:
Expand: добавьте новую колонку или таблицу так, чтобы старая версия приложения продолжала работать.
Dual-write или совместимость: приложение на время пишет и читает так, чтобы поддерживать оба формата.
Contract: когда старый код полностью выключен, удалите старые поля и ограничения.!Как менять схему БД без простоя и поломки старых версий приложения
Примеры SQL-миграций
Создание таблиц и ограничений:
Добавление поля (шаг expand):
Важно: добавление NOT NULL и “тяжёлых” индексов на больших таблицах может быть небезопасным без подготовки, потому что операция может занять много времени и блокировать запросы. В продакшене такие изменения планируют отдельно.
Инструменты для миграций
Инструмент зависит от языка и стека, но смысл одинаков: применять миграции по порядку и хранить версию схемы.
Примеры популярных инструментов:
Flyway Documentation
Liquibase Documentation
Alembic Documentation
Django Migrations
Prisma Migrate
golang-migrate/migrateКак слой данных связан с архитектурой проекта
С учётом структуры из прошлой статьи полезно держать следующие границы:
HTTP-слой не строит SQL и не знает деталей схемы
use case оперирует бизнес-сущностями и правилами
репозиторий отвечает за запросы к БД и преобразование данных в доменную модельЭто позволяет:
тестировать бизнес-логику без реальной базы
менять схему и оптимизировать запросы без переписывания контроллеров
поддерживать несколько хранилищ (например, SQL для основной модели и Redis для кэша)Минимальный чеклист перед продакшеном
Схема данных отражает реальные запросы и сценарии.
Есть ограничения целостности: ключи, уникальность, обязательность.
Для горячих запросов предусмотрены индексы.
Есть миграции в репозитории и автоматическое применение в CI.
Изменения схемы планируются как backward compatible, если деплой не атомарный.
Кэш (например, Redis) используется как ускорение, а не как единственный источник истины, если данные критичны.