Когда мы идеально нормализуем данные, каждая таблица становится максимально компактной, и информация в ней отвечает строго одному принципу. Однако для выполнения реальных запросов (например, "Какие студенты записаны на курс 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 вручную. Это может привести к несоответствиям.
Аномалии при вставке, обновлении и удалении
Информация легко "рассинхронизируется", если забыть обновить денормализованные данные. Например, если меняется цена продукта, это происходит не автоматически.
Баланс: как найти золотую середину?
Выберите, что важнее: производительность или структура? Если база активно читается, подстраивайтесь под запросы.
Добавляйте денормализацию точечно. Например, только для ключевых чисел и отчетов.
Автоматизируйте обновления денормализованных данных. Используйте триггеры или задачи, чтобы избежать несоответствий.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ