JavaRush /Курсы /SQL SELF /Баланс между нормализацией и производительностью

Баланс между нормализацией и производительностью

SQL SELF
26 уровень , 3 лекция
Открыта

Когда мы идеально нормализуем данные, каждая таблица становится максимально компактной, и информация в ней отвечает строго одному принципу. Однако для выполнения реальных запросов (например, "Какие студенты записаны на курс SQL?") может понадобиться объединить кучу таблиц. Чем больше таблиц, тем сложнее запросы, тем больше система "работает лопатой".

Наверняка вам уже знакомы JOIN-ы из предыдущих лекций. Вот пример запроса, который может понадобиться при работе с нормально спроектированной базой данных:

SELECT students.name, courses.title
FROM students
JOIN enrollments ON students.id = enrollments.student_id
JOIN courses ON enrollments.course_id = courses.id
WHERE courses.title = 'SQL';

Звучит просто, но под капотом сервер выполняет адскую работу: чтение каждой таблицы, соединение данных, фильтрация... А что если таблицы очень-очень большие? Логично предположить, что производительность будет падать.

Войнушки: нормализация против скорости

К счастью (или к сожалению?), в реальной жизни базы данных — это компромисс. Полная нормализация обеспечивает целостность данных, но замедляет выполнение сложных запросов. Если база данных будет использоваться для аналитики и отчетов, то иногда выгоднее денормализовать её. Это как заменить 10 маленьких коробочек на один большой сундук: доставать данные быстрее, но разложить их заново станет сложнее.

Когда стоит "выдохнуть" с нормализацией?

Существуют сценарии, где денормализация предпочтительнее:

Часто используемые агрегаты

Например, представьте, что система ежедневно делает запросы на подсчет количества студентов на каждом курсе. В нормализованной структуре потребуется выполнять JOIN и COUNT() постоянно. Вместо этого можно добавить в таблицу "Courses" колонку student_count, обновляемую автоматически при добавлении/удалении записей.

-- Денормализованный столбец
UPDATE courses
SET student_count = (
    SELECT COUNT(*)
    FROM enrollments
    WHERE enrollments.course_id = courses.id
);

Часто выполняемые отчеты

Если ваш клиент каждый день хочет отчет "Кто, где, когда покупал?", проще хранить денормализованную таблицу с готовыми строками вида "Имя клиента, товар, дата". Основная таблица будет больше, но скорость получения данных увеличится.

Большие объемы чтения, малые объемы записи

Когда база данных в основном используется для чтения (например, аналитики), стоит пожертвовать нормализацией ради скорости.

Минимизация соединений при сложных связях

Если между таблицами многослойные (nested) связи, и JOIN становится кошмаром, уберите некоторые уровни нормализации.

Пример: как денормализация ускоряет работу?

Имеем нормализованные таблицы интернет-магазина:

Таблица products Таблица orders Таблица order_items
id id id
name date order_id
price customer_id product_id
quantity

Каждый заказ (orders) состоит из строк заказа (order_items). Посчитаем, сколько денег заработал магазин:

SELECT SUM(order_items.quantity * products.price) AS total_revenue
FROM order_items
JOIN products ON order_items.product_id = products.id;

Процесс соединения order_items и products замедлит запрос на больших объемах данных.

Денормализованная структура

Теперь представим, что в таблице order_items есть "лишняя" колонка total_price (денормализация):

Таблица order_items
id
order_id
product_id
quantity
total_price

Теперь запрос становится тривиальным:

SELECT SUM(total_price) AS total_revenue
FROM order_items;

Так мы избегаем JOIN, а значит ускоряем выполнение.

Практическое задание: оптимизация базы "Продажи"

Дано: нормализованные таблицы

Таблица products Таблица sales
id id
name product_id
price date
quantity

Задача: ускорить частые запросы вида "Сколько всего заработали на каждом продукте?".

Шаг 1: добавим колонку total_price в таблицу sales:

ALTER TABLE sales ADD COLUMN total_price NUMERIC;

Шаг 2: заполним эту колонку при существующих данных:

UPDATE sales
SET total_price = quantity * (
    SELECT price
    FROM products
    WHERE products.id = sales.product_id
);

Шаг 3: делайте запросы быстрее:

SELECT product_id, SUM(total_price) AS total_revenue
FROM sales
GROUP BY product_id;

Но! У денормализации есть цена

Вы же понимаете, что "быстрее" не всегда значит "лучше". В случае денормализации появляются проблемы:

Избыточное хранилище

Колонка total_price — это копия данных, требующая дополнительного места.

Сложность обновлений

Если цена продукта изменится в таблице products, мы должны обновить соответствующую колонку total_price вручную. Это может привести к несоответствиям.

Аномалии при вставке, обновлении и удалении

Информация легко "рассинхронизируется", если забыть обновить денормализованные данные. Например, если меняется цена продукта, это происходит не автоматически.

Баланс: как найти золотую середину?

Выберите, что важнее: производительность или структура? Если база активно читается, подстраивайтесь под запросы.

Добавляйте денормализацию точечно. Например, только для ключевых чисел и отчетов.

Автоматизируйте обновления денормализованных данных. Используйте триггеры или задачи, чтобы избежать несоответствий.

2
Задача
SQL SELF, 26 уровень, 3 лекция
Недоступна
Денормализация для ускорения подсчёта
Денормализация для ускорения подсчёта
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 35 Student
31 июля 2025
Имхо тут ключевое слово - триггеры. Юзайте нормализованные базы с денормализованными агрегированными? полями, которые считаются триггерами, а не вручную.
Alexandr Уровень 61
20 июля 2025
Задача "Денормализация для ускорения подсчёта": оператор JOIN здесь не нужен. В правильном ответе его также нет. Хотя в валидаторе это условие присутствует. Странно - считаю, нужно пофиксить.
Евгений Уровень 49 Expert
26 августа 2025
Видимо, они опять перепутали базы данных. Для MySQL и SQL Server можно обновлять через JOIN: ссылка.
Юрий Уровень 60
8 ноября 2025
в postgresql можно базы join-ить не указывая на прямую слово join

select * from member m, user u
where m.user_id = u.id
Лично мне так не нравится можно join пропустить но работает