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

Приведи пример когда нормализация будет лишней

1.0 Junior🔥 181 комментариев
#Python Core

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

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

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

Когда нормализация базы данных лишняя

Нормализация — золотой стандарт дизайна БД, но есть ситуации, когда она замораживает систему. Давайте разберёмся, когда денормализация имеет смысл.

Проблема: нормализованная БД для аналитики

Нормализованная схема:

# Таблица: orders (1M записей)
id, user_id, created_at, total_amount

# Таблица: order_items (5M записей)
id, order_id, product_id, quantity, price

# Таблица: products (50K записей)
id, name, category_id, price

# Таблица: categories (100 записей)
id, name

# Таблица: users (100K записей)
id, name, email, city_id, country_id

# Таблица: cities (500 записей)
id, name

# Таблица: countries (200 записей)
id, name

Типичный аналитический запрос:

SELECT 
    countries.name,
    cities.name,
    DATE(orders.created_at) as order_date,
    COUNT(orders.id) as orders_count,
    SUM(order_items.quantity) as items_sold,
    AVG(order_items.price) as avg_price
FROM orders
JOIN order_items ON orders.id = order_items.order_id
JOIN products ON order_items.product_id = products.id
JOIN users ON orders.user_id = users.id
JOIN cities ON users.city_id = cities.id
JOIN countries ON cities.country_id = countries.id
GROUP BY countries.name, cities.name, DATE(orders.created_at);

Проблемы:

  • 6 JOINов для одного отчёта
  • Query planner тратит время на оптимизацию
  • Кэширование страниц тяжело
  • На 1M заказов это медленно

Денормализация: аналитическая таблица

Вместо JOINов — денормализованная таблица:

# Таблица: order_analytics (5M записей)
order_id
user_id
product_id
order_date
order_time
country_name          # Копия из countries
city_name             # Копия из cities
user_name             # Копия из users
product_name          # Копия из products
product_category_name # Копия из categories
quantity
item_price
order_total_amount

Теперь запрос:

SELECT 
    country_name,
    city_name,
    order_date,
    COUNT(*) as orders_count,
    SUM(quantity) as items_sold,
    AVG(item_price) as avg_price
FROM order_analytics
GROUP BY country_name, city_name, order_date;

Результаты:

  • 0 JOINов
  • В 100x быстрее!
  • Просто GROUP BY + aggreation

Пример 1: E-commerce — денормализация для product card

Нормализованный подход:

# SELECT product
product = Product.objects.get(id=123)
# SELECT category
category = Category.objects.get(id=product.category_id)
# SELECT reviews count
review_count = Review.objects.filter(product_id=123).count()
# SELECT average rating
avg_rating = Review.objects.filter(product_id=123).aggregate(Avg('rating'))
# SELECT seller
seller = Seller.objects.get(id=product.seller_id)

# Итого: 5 SQL запросов для одной страницы товара

Денормализованный подход:

# В таблице product храним:
id
name
price
category_name          # Копируем из categories
review_count          # Храним число, обновляем триггером
average_rating         # Храним число, обновляем триггером
seller_name            # Копируем из sellers

# SELECT product
product = Product.objects.get(id=123)  # 1 запрос!
print(f"{product.name} ({product.average_rating})")
print(f"Category: {product.category_name}")
print(f"Seller: {product.seller_name}")

Как синхронизировать денормализованные данные:

# Триггер в PostgreSQL
CREATE TRIGGER update_product_stats
AFTER INSERT OR DELETE ON reviews
FOR EACH ROW
EXECUTE FUNCTION update_product_denormalized_stats();

CREATE FUNCTION update_product_denormalized_stats() RETURNS TRIGGER AS $$
BEGIN
    UPDATE products
    SET 
        review_count = (SELECT COUNT(*) FROM reviews WHERE product_id = NEW.product_id),
        average_rating = (SELECT AVG(rating) FROM reviews WHERE product_id = NEW.product_id)
    WHERE id = NEW.product_id;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Или в приложении (более гибко):

def create_review(product_id: int, rating: float):
    review = Review.objects.create(product_id=product_id, rating=rating)
    
    # Обновляем денормализованные данные
    product = Product.objects.get(id=product_id)
    product.review_count = Review.objects.filter(product_id=product_id).count()
    product.average_rating = Review.objects.filter(product_id=product_id).aggregate(Avg('rating'))['rating__avg']
    product.save(update_fields=['review_count', 'average_rating'])
    
    return review

Пример 2: Кэш-слой вместо нормализации

Проблема: получить статистику юзера медленно

# Нормализованная БД
users (id, name)
user_posts (user_id, post_id)
posts (id, title, created_at)
post_likes (post_id, user_id)
comments (id, post_id, user_id)

# Получить профиль юзера:
SELECT 
    u.name,
    COUNT(DISTINCT up.post_id) as posts_count,
    COUNT(DISTINCT pl.user_id) as likes_count,
    COUNT(DISTINCT c.id) as comments_count
FROM users u
LEFT JOIN user_posts up ON u.id = up.user_id
LEFT JOIN posts p ON up.post_id = p.id
LEFT JOIN post_likes pl ON p.id = pl.post_id AND pl.user_id = u.id
LEFT JOIN comments c ON p.id = c.post_id AND c.user_id = u.id
WHERE u.id = 123

# Это ОЧЕНЬ медленно! Много JOINов и GROUP BY

Денормализованный подход: Redis кэш

def get_user_stats(user_id: int):
    # Попробуем из кэша
    cache_key = f"user:stats:{user_id}"
    cached = redis.get(cache_key)
    if cached:
        return json.loads(cached)  # Мгновенно!
    
    # Если нет в кэше, вычислим
    stats = {
        'posts_count': user_posts.count(),
        'likes_count': post_likes.count(),
        'comments_count': comments.count()
    }
    
    # Сохраним в кэш на 1 час
    redis.setex(cache_key, 3600, json.dumps(stats))
    return stats

# Обновляем кэш при изменениях
def create_post(user_id, title):
    post = Post.objects.create(user_id=user_id, title=title)
    redis.delete(f"user:stats:{user_id}")  # Инвалидируем кэш
    return post

Пример 3: Аналитический хранилище (Data Warehouse)

OLTP БД (нормализованная):

# Быстро пишем данные
# Много нормализации
# SELECT * FROM order_items WHERE order_id = 123 быстро

Data Warehouse (денормализованная, OLAP):

# Таблица: fact_sales (денормализованная)
# Содержит всю информацию для аналитики
date
user_id
user_name
user_email
user_city_name
user_country_name
product_id
product_name
product_category_name
product_price
seller_id
seller_name
quantity
order_amount

# Теперь любой аналитический запрос очень быстро:
SELECT user_country_name, SUM(quantity), AVG(order_amount)
FROM fact_sales
GROUP BY user_country_name

# Это стандартная практика в Snowflake, BigQuery, Redshift

Когда НЕ денормализовать

Если данные изменяются часто:

# Если rating товара меняется каждую секунду
# Денормализация создаёт проблемы синхронизации
# Лучше просто SELECT COUNT

Если данные консистентности критичны:

# Финансовые операции
# Баланс счёта
# Нужна абсолютная точность
# Нельзя заморозить на 5 минут

Если место на диске критично:

# Денормализация = дублирование данных
# Увеличивается размер БД в 2-3 раза
# Если это критично — избегаем

Вывод

Денормализация имеет смысл:

  • В аналитических системах
  • Для часто читаемых данных
  • Когда производительность SELECT критична
  • Когда есть триггеры/кэш для синхронизации

Не денормализовать:

  • Если изменяемость данных высокая
  • Если нужна абсолютная консистентность
  • Если это усложняет код

Общее правило: Нормализовать OLTP, денормализовать OLAP.