← Назад к вопросам
Приведи пример ненормализованных данных
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 системах, но требует аккуратного управления обновлениями, чтобы избежать несогласованности данных.