> Что такое нормализация и денормализация баз данных (Go)

Уровень: junior · Роль: backend · Категория: Технические вопросы

Компании: amoCRM, Wildberries, Lamoda, Employcity

Стек: Go

> Пример ответа

Короткий ответ

Нормализация - это процесс организации данных в БД для устранения избыточности и аномалий через разбиение таблиц и установление связей. Денормализация - намеренное добавление избыточности (дублирование данных, лишние колонки) для ускорения чтения за счёт уменьшения JOIN-ов. В backend-разработке нормализация обеспечивает целостность, денормализация - производительность. Выбор зависит от нагрузки: OLTP тяготеет к нормализации, OLAP и read-heavy системы - к денормализации.

Подробное объяснение

Нормализация - это формальный процесс приведения схемы БД к нормальным формам (1NF, 2NF, 3NF, BCNF и выше). Основная цель - устранить избыточность и аномалии вставки, обновления и удаления. Например, если хранить имя автора в каждой строке книги, то при смене имени придётся обновлять все строки - это аномалия обновления. Нормализация решает это через отдельную таблицу авторов и внешний ключ.

Денормализация - обратный процесс: сознательное нарушение нормальных форм для оптимизации производительности. Типичные приёмы: добавление вычисляемых колонок (например, total_price), дублирование справочных данных (имя автора прямо в таблице книг), создание сводных таблиц (summary tables), материализованные представления.

Ключевой trade-off: нормализация экономит место и гарантирует консистентность, но требует JOIN-ов при чтении. Денормализация ускоряет чтение, но усложняет запись (нужно обновлять несколько мест) и увеличивает риск рассинхронизации данных.

На практике в Go-проектах часто используют гибридный подход: нормализованная схема для транзакционных операций и денормализованные read models (например, через CQRS или отдельные таблицы для отчётов). Также важно помнить, что современные БД (PostgreSQL) поддерживают материализованные представления, которые дают денормализацию без ручного дублирования.

На практике

Для junior-позиции важно понимать, когда применять каждый подход:

  • Нормализация - стандарт для OLTP-систем, где важна целостность данных (заказы, платежи, пользователи).
  • Денормализация - для read-heavy сценариев: ленты новостей, аналитические дашборды, кэширующие таблицы.
  • В Go-проектах часто используют миграции (golang-migrate, goose) для управления схемой - там и принимаются решения о нормализации/денормализации.
  • При денормализации в Go нужно продумать стратегию синхронизации: либо обновление в одной транзакции (если БД поддерживает), либо фоновые задачи (worker-ы), либо event-driven подход.

Пример из практики: в интернет-магазине таблица orders нормализована (отдельные таблицы для клиентов, товаров, заказов), но для страницы "Мои заказы" создают денормализованную таблицу order_summary с уже готовыми полями (имя клиента, сумма, статус), чтобы не делать 4 JOIN-а на каждый запрос.

Пример кода

Допустим, у нас нормализованная схема:

SQL
-- Нормализованная схема
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INT REFERENCES authors(id)
);

Запрос для получения книги с автором:

GO
type BookWithAuthor struct {
BookID int
Title string
AuthorID int
Author string
}
func GetBookWithAuthor(ctx context.Context, db *sql.DB, bookID int) (*BookWithAuthor, error) {
row := db.QueryRowContext(ctx, `
SELECT b.id, b.title, b.author_id, a.name
FROM books b
JOIN authors a ON a.id = b.author_id
WHERE b.id = $1
`, bookID)
var result BookWithAuthor
err := row.Scan(&result.BookID, &result.Title, &result.AuthorID, &result.Author)
if err != nil {
return nil, err
}
return &result, nil
}

Денормализованная схема для read-heavy сценария:

SQL
-- Денормализованная таблица для чтения
CREATE TABLE book_read_model (
book_id INT PRIMARY KEY,
title TEXT NOT NULL,
author_name TEXT NOT NULL -- дублирование
);

Запрос без JOIN:

GO
func GetBookFast(ctx context.Context, db *sql.DB, bookID int) (*BookWithAuthor, error) {
row := db.QueryRowContext(ctx, `
SELECT book_id, title, author_name
FROM book_read_model
WHERE book_id = $1
`, bookID)
var result BookWithAuthor
err := row.Scan(&result.BookID, &result.Title, &result.Author)
if err != nil {
return nil, err
}
return &result, nil
}

Как отвечать на собеседовании

Начните с чёткого определения обоих понятий. Затем приведите конкретный пример аномалии, которую устраняет нормализация. После этого объясните, зачем нужна денормализация, и обязательно упомяните trade-off: целостность против скорости чтения. Хорошо, если вы приведёте реальный сценарий из своего опыта или типовой паттерн (read model, CQRS). Для junior достаточно уверенно объяснить 1NF-3NF и показать понимание, когда денормализация оправдана. Не углубляйтесь в 4NF/5NF, если не спрашивают.

Что проверяет интервьюер

  • Понимание базовых концепций реляционных БД: ключи, связи, аномалии.
  • Умение объяснить trade-off, а не просто заученные определения.
  • Практическое мышление: может ли кандидат определить, когда нормализация вредит производительности.
  • Знание инструментов: индексы, материализованные представления, миграции.
  • Для Go-позиции - способность связать выбор схемы с паттернами доступа к данным в приложении.

Типичные ошибки

  • Путаница между нормализацией и индексами: индексы не заменяют нормализацию, это разные вещи.
  • Утверждение, что денормализация - это всегда плохо. На самом деле это осознанный компромисс.
  • Непонимание, что денормализация усложняет запись: если обновление происходит в нескольких местах, нужна транзакция или фоновые задачи.
  • Отсутствие примеров: ответ без конкретики выглядит слабо.
  • Перечисление всех нормальных форм без объяснения, зачем они нужны на практике.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью