Приведи пример когда нужна денормализация
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Примеры когда нужна денормализация БД
Что такое денормализация
Денормализация — это намеренное нарушение нормальных форм базы данных путём добавления избыточных данных для повышения производительности чтения.
Реальные примеры
1. Кэширование агрегированных данных в таблице профиля
Проблема: Каждый раз при загрузке профиля нужно считать статистику из 5 таблиц.
-- Нормализованный подход (медленный)
SELECT
u.id, u.name,
COUNT(p.id) as posts_count,
COUNT(c.id) as comments_count,
COUNT(l.id) as likes_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
LEFT JOIN comments c ON u.id = c.user_id
LEFT JOIN likes l ON u.id = l.user_id
WHERE u.id = $1
GROUP BY u.id;
-- Денормализованный подход (быстрый)
SELECT id, name, posts_count, comments_count, likes_count
FROM users
WHERE id = $1;
Решение: Добавляем столбцы-кэши в таблицу users:
posts_countcomments_countlikes_count
Обновляются триггерами или фоновыми задачами:
# Celery задача для периодического обновления
@shared_task
def update_user_statistics(user_id: int):
stats = calculate_user_stats(user_id)
User.objects.filter(id=user_id).update(**stats)
2. Кэширование данных пользователя в заказе
Проблема: При загрузке заказа нужно джойнить с таблицей users, адресов, платежных методов.
-- Нормализованно
SELECT
o.id, o.total,
u.id, u.name, u.email,
a.street, a.city, a.zip,
p.card_last_four
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN addresses a ON o.address_id = a.id
JOIN payments p ON o.payment_id = p.id;
-- Денормализованно
SELECT *
FROM orders
WHERE id = $1;
Решение: Дублируем данные в заказе:
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
total = Column(Numeric)
# Денормализованные данные клиента
user_name = Column(String) # Копия из users.name
user_email = Column(String) # Копия из users.email
# Денормализованные адресные данные
shipping_street = Column(String)
shipping_city = Column(String)
shipping_zip = Column(String)
# Денормализованные данные платежа
payment_method_last_four = Column(String)
Это нужно, чтобы заказ содержал моментальный снимок данных — даже если позже пользователь изменит имя, заказ сохранит историю.
3. Таблица уведомлений с предварительно сформированным текстом
Проблема: Создание уведомления требует сложной логики вычисления текста, иконки, цвета.
-- Нормализованно
SELECT
n.id,
n.type,
u.name,
a.title
FROM notifications n
JOIN users u ON n.actor_id = u.id
JOIN articles a ON n.article_id = a.id;
-- Денормализованно
SELECT id, text, icon, color, action_url
FROM notifications;
Решение: Сохраняем готовый текст при создании:
class Notification(Base):
__tablename__ = "notifications"
id = Column(Integer, primary_key=True)
# Готовые данные для отображения
title = Column(String) # "Иван лайкнул вашу статью"
text = Column(String) # Полный текст уведомления
icon_url = Column(String) # URL иконки
action_url = Column(String) # URL для перехода
# Оригинальные FK
actor_id = Column(Integer, ForeignKey("users.id"))
article_id = Column(Integer, ForeignKey("articles.id"))
4. Счётчики в социальных сетях
Проблема: Каждый раз считать лайки, комментарии, репосты из отдельных таблиц очень медленно.
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
content = Column(String)
# Денормализованные счётчики
likes_count = Column(Integer, default=0)
comments_count = Column(Integer, default=0)
reposts_count = Column(Integer, default=0)
# Обновляются триггерами при создании like/comment
Триггер в PostgreSQL:
CREATE TRIGGER increment_likes_count
AFTER INSERT ON likes
FOR EACH ROW
EXECUTE FUNCTION increment_post_likes();
CREATE FUNCTION increment_post_likes()
RETURNS TRIGGER AS $$
BEGIN
UPDATE posts SET likes_count = likes_count + 1
WHERE id = NEW.post_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Когда использовать денормализацию
✅ Используй денормализацию когда:
- Чтение данных происходит в 100x чаще, чем запись
- JOIN запрос вызывает заметные задержки
- Данные редко изменяются
- Можешь обновлять денормализованные данные триггерами или фоновыми задачами
❌ Избегай денормализации когда:
- Данные часто меняются
- Синхронизация между копиями сложна
- Проблему можно решить индексами или кэшированием
Инструменты синхронизации
# Вариант 1: SQLAlchemy события
from sqlalchemy import event
@event.listens_for(Like, after_insert)
def receive_after_insert(mapper, connection, target):
connection.execute(
update(Post).where(Post.id == target.post_id)
.values(likes_count=Post.likes_count + 1)
)
# Вариант 2: Celery задача
@shared_task
def update_post_stats(post_id: int):
likes = db.session.query(Like).filter(Like.post_id == post_id).count()
Post.query.filter(Post.id == post_id).update({Post.likes_count: likes})
db.session.commit()
# Вариант 3: Триггеры БД (лучший вариант)
CREATE TRIGGER sync_likes_count AFTER INSERT ON likes
UPDATE posts SET likes_count = likes_count + 1 WHERE id = NEW.post_id;
Вывод
Денормализация — это мощный инструмент для оптимизации, но требует дисциплины. Используй её только когда нормализованный подход создаёт настоящие проблемы с производительностью, и всегда обеспечивай синхронизацию данных триггерами или фоновыми задачами.