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

Приведи пример ненормализованных данных

2.2 Middle🔥 181 комментариев
#Базы данных (SQL)

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

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

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

# Пример ненормализованных данных

Денормализованные данные — это намеренное нарушение нормальной формы БД для повышения производительности чтения в ущерб сложности обновления. Покажу примеры и объясню trade-offs.

1. Простой пример: данные о заказах

❌ Ненормализованная структура (денормализация)

CREATE TABLE orders_denormalized (
    order_id INTEGER PRIMARY KEY,
    order_date DATE,
    customer_id INTEGER,
    customer_name VARCHAR,         -- Дублирует данные из таблицы customers
    customer_email VARCHAR,        -- Дублирует данные из таблицы customers
    customer_phone VARCHAR,        -- Дублирует данные из таблицы customers
    product_id INTEGER,
    product_name VARCHAR,          -- Дублирует данные из таблицы products
    product_price DECIMAL,         -- Дублирует данные из таблицы products
    quantity INTEGER,
    total_amount DECIMAL,          -- Можно вычислить: product_price * quantity
    status VARCHAR
);

-- Данные
INSERT INTO orders_denormalized VALUES
(1, '2024-01-15', 10, 'Alice Johnson', 'alice@example.com', '+1-555-0001', 100, 'Laptop', 999.99, 1, 999.99, 'completed'),
(2, '2024-01-16', 20, 'Bob Smith', 'bob@example.com', '+1-555-0002', 101, 'Mouse', 29.99, 2, 59.98, 'completed'),
(3, '2024-01-17', 10, 'Alice Johnson', 'alice@example.com', '+1-555-0001', 102, 'Keyboard', 79.99, 1, 79.99, 'pending');

Проблемы:

  • 📍 Аномалия обновления: если Alice сменила email, нужно обновить 2 строки (заказ 1 и 3)
  • 🗑️ Аномалия удаления: если удалим заказ 3, потеряем информацию о клиенте Alice
  • 💾 Избыточные данные: имя Alice повторяется в каждом её заказе
  • ⚠️ Несогласованность: возможна ситуация, где в разных заказах у Alice разные email'ы

✅ Нормализованная структура (3-я нормальная форма)

CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY,
    customer_name VARCHAR,
    customer_email VARCHAR,
    customer_phone VARCHAR
);

CREATE TABLE products (
    product_id INTEGER PRIMARY KEY,
    product_name VARCHAR,
    product_price DECIMAL
);

CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    order_date DATE,
    customer_id INTEGER REFERENCES customers(customer_id),
    product_id INTEGER REFERENCES products(product_id),
    quantity INTEGER,
    status VARCHAR
);

-- Данные
INSERT INTO customers VALUES
(10, 'Alice Johnson', 'alice@example.com', '+1-555-0001'),
(20, 'Bob Smith', 'bob@example.com', '+1-555-0002');

INSERT INTO products VALUES
(100, 'Laptop', 999.99),
(101, 'Mouse', 29.99),
(102, 'Keyboard', 79.99);

INSERT INTO orders VALUES
(1, '2024-01-15', 10, 100, 1, 'completed'),
(2, '2024-01-16', 20, 101, 2, 'completed'),
(3, '2024-01-17', 10, 102, 1, 'pending');

Преимущества:

  • ✅ Нет дублирования данных
  • ✅ Одно место изменения: если Alice сменила email, меняем только в таблице customers
  • ✅ Нет аномалий
  • ✅ Экономия памяти

2. Реальный пример: кэширование данных в документной БД

Сценарий: интернет-магазин с MongoDB

// ❌ Ненормализованная структура (денормализация для быстрого чтения)
db.posts.insertOne({
    _id: ObjectId("6501abc1234567890"),
    title: "Top 10 Python Tips",
    content: "Lorem ipsum...",
    author_id: ObjectId("64f5def1234567890"),
    author_name: "John Doe",                      // Дублированные данные
    author_email: "john@example.com",            // Дублированные данные
    author_avatar_url: "https://...",            // Дублированные данные
    author_bio: "Software developer with 10+ years experience",  // Дублированные
    comments: [
        {
            _id: ObjectId("6501abc1234567891"),
            commenter_id: ObjectId("64f5def1234567891"),
            commenter_name: "Jane Smith",         // Дублированные данные
            commenter_avatar_url: "https://...",  // Дублированные данные
            text: "Great article!",
            likes_count: 5,                         // Может отличаться от реального
            created_at: ISODate("2024-01-15T10:00:00Z")
        }
    ],
    likes_count: 120,                              // Вычисляемое поле (может быть некорректно)
    views_count: 5000,                             // Вычисляемое поле
    tags: ["python", "programming", "tips"],
    created_at: ISODate("2024-01-15T08:00:00Z"),
    updated_at: ISODate("2024-01-16T10:00:00Z")
});

Проблемы денормализации:

# Сценарий: автор (John) сменил имя на "Jonathan Doe" и аватар
# ❌ Нужно обновить все документы, где автор упоминается:

db.posts.update_many(
    {'author_id': ObjectId("64f5def1234567890")},
    {{
        '$set': {
            'author_name': 'Jonathan Doe',
            'author_avatar_url': 'https://new-avatar.jpg'
        }
    }}
);

# Но также нужно обновить все комментарии этого автора во всех постах:
db.posts.update_many(
    {'comments.commenter_id': ObjectId("64f5def1234567890")},
    {{
        '$set': {
            'comments.$[elem].commenter_name': 'Jonathan Doe',
            'comments.$[elem].commenter_avatar_url': 'https://new-avatar.jpg'
        }
    }},
    {arrayFilters: [{'elem.commenter_id': ObjectId("64f5def1234567890")}]}
);

# ⚠️ Если забыть обновить хотя бы одно место, появится несогласованность

✅ Нормализованная структура

// Таблица пользователей
db.users.insertOne({
    _id: ObjectId("64f5def1234567890"),
    name: "John Doe",
    email: "john@example.com",
    avatar_url: "https://...",
    bio: "Software developer with 10+ years experience",
    created_at: ISODate("2023-06-15T10:00:00Z")
});

// Таблица постов (только ссылка на автора)
db.posts.insertOne({
    _id: ObjectId("6501abc1234567890"),
    title: "Top 10 Python Tips",
    content: "Lorem ipsum...",
    author_id: ObjectId("64f5def1234567890"),  // Ссылка, не дублирование
    comments: [
        {
            _id: ObjectId("6501abc1234567891"),
            commenter_id: ObjectId("64f5def1234567891"),  // Ссылка
            text: "Great article!",
            likes_count: 5,
            created_at: ISODate("2024-01-15T10:00:00Z")
        }
    ],
    likes: [ObjectId("..."), ObjectId("...")],  // Список ID пользователей, которые лайкали
    views: [ObjectId("..."), ObjectId("...")],  // Список ID пользователей, которые просмотрели
    tags: ["python", "programming", "tips"],
    created_at: ISODate("2024-01-15T08:00:00Z"),
    updated_at: ISODate("2024-01-16T10:00:00Z")
});

Обновление просто:

# Обновляем только таблицу users
db.users.update_one(
    {'_id': ObjectId("64f5def1234567890")},
    {{
        '$set': {
            'name': 'Jonathan Doe',
            'avatar_url': 'https://new-avatar.jpg'
        }
    }}
);

# При чтении используем $lookup (JOIN)
db.posts.aggregate([
    {'$lookup': {
        'from': 'users',
        'localField': 'author_id',
        'foreignField': '_id',
        'as': 'author'
    }}
])

3. Практический пример: e-commerce с Python

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

from pymongo import MongoClient
from datetime import datetime

client = MongoClient()
db = client['ecommerce']

# Денормализованная коллекция заказов
order = {
    '_id': 'order_12345',
    'order_date': datetime.now(),
    'customer_id': 'cust_001',
    'customer_name': 'Alice Johnson',           # Дублировано
    'customer_email': 'alice@example.com',      # Дублировано
    'customer_address': '123 Main St',          # Дублировано
    'customer_phone': '+1-555-0001',            # Дублировано
    'items': [
        {
            'product_id': 'prod_100',
            'product_name': 'Laptop',            # Дублировано
            'product_price': 999.99,             # Дублировано (может измениться)
            'quantity': 1,
            'subtotal': 999.99
        },
        {
            'product_id': 'prod_101',
            'product_name': 'Mouse',             # Дублировано
            'product_price': 29.99,              # Дублировано
            'quantity': 2,
            'subtotal': 59.98
        }
    ],
    'total_amount': 1059.97,                    # Вычисляемое поле
    'shipping_address': '123 Main St',          # Дублировано
    'status': 'completed'
}

db.orders.insert_one(order)

# Проблема: если изменится цена товара или информация клиента,
# исторические заказы будут неточными

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

# Таблица клиентов
db.customers.insert_one({
    '_id': 'cust_001',
    'name': 'Alice Johnson',
    'email': 'alice@example.com',
    'phone': '+1-555-0001',
    'addresses': [
        {'type': 'home', 'address': '123 Main St'},
        {'type': 'work', 'address': '456 Work Ave'}
    ]
})

# Таблица товаров
db.products.insert_one({
    '_id': 'prod_100',
    'name': 'Laptop',
    'current_price': 999.99,
    'category': 'Electronics'
})

# Таблица заказов (только ссылки)
db.orders.insert_one({
    '_id': 'order_12345',
    'order_date': datetime.now(),
    'customer_id': 'cust_001',
    'items': [
        {
            'product_id': 'prod_100',
            'quantity': 1,
            'price_at_purchase': 999.99  # Цена в момент покупки
        },
        {
            'product_id': 'prod_101',
            'quantity': 2,
            'price_at_purchase': 29.99
        }
    ],
    'status': 'completed'
})

# Чтение заказа с информацией клиента и товаров
order_with_details = db.orders.aggregate([
    {'$match': {'_id': 'order_12345'}},
    {'$lookup': {
        'from': 'customers',
        'localField': 'customer_id',
        'foreignField': '_id',
        'as': 'customer'
    }},
    {'$unwind': '$items'},
    {'$lookup': {
        'from': 'products',
        'localField': 'items.product_id',
        'foreignField': '_id',
        'as': 'product'
    }}
])

4. Когда денормализация оправдана?

Денормализуй, если:

  • Чтение данных происходит в 100x раз чаще, чем обновление
  • Производительность критична (e-commerce, высоконагруженные системы)
  • JOIN'ы убивают производительность
  • Историческая точность менее важна, чем скорость

Не денормализуй, если:

  • Данные часто обновляются
  • Нужна абсолютная согласованность
  • Памяти хватает для нормализованной схемы
  • Система небольшая (< миллионов записей)

5. Компромисс: частичная денормализация

# Хороший баланс: денормализуем только часто читаемые данные
db.orders.insert_one({
    '_id': 'order_12345',
    'customer_id': 'cust_001',
    'customer_name': 'Alice Johnson',  # Денормализуем для быстрого отображения
    'customer_email': 'alice@example.com',  # Денормализуем для быстрого отображения
    'items': [
        {
            'product_id': 'prod_100',
            'product_name': 'Laptop',  # Денормализуем название
            'quantity': 1,
            'price_at_purchase': 999.99  # Сохраняем цену в момент покупки
        }
    ],
    'status': 'completed'
})

# Полную информацию (адреса, полные профили) получаем через JOIN при необходимости

Вывод

Ненормализованные данные содержат дублирование для повышения производительности чтения. Это полезно в high-load системах, но требует аккуратного управления обновлениями, чтобы избежать несогласованности данных.