← Назад к вопросам

Приведи пример когда нужна денормализация

2.0 Middle🔥 191 комментариев
#Архитектура и паттерны#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Примеры когда нужна денормализация БД

Что такое денормализация

Денормализация — это намеренное нарушение нормальных форм базы данных путём добавления избыточных данных для повышения производительности чтения.

Реальные примеры

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_count
  • comments_count
  • likes_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;

Вывод

Денормализация — это мощный инструмент для оптимизации, но требует дисциплины. Используй её только когда нормализованный подход создаёт настоящие проблемы с производительностью, и всегда обеспечивай синхронизацию данных триггерами или фоновыми задачами.

Приведи пример когда нужна денормализация | PrepBro