← Назад к вопросам
В чем смысл нормализации данных?
2.0 Middle🔥 191 комментариев
#Архитектура и паттерны#Базы данных (SQL)
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Нормализация данных
Нормализация — это процесс организации данных в базе для минимизации дублирования, обеспечения целостности и улучшения производительности запросов.
Проблема: денормализованные данные
-- Плохо: все данные в одной таблице
CREATE TABLE orders_denorm (
id SERIAL PRIMARY KEY,
order_date DATE,
customer_name VARCHAR,
customer_email VARCHAR,
customer_phone VARCHAR,
customer_address VARCHAR,
product_name VARCHAR,
product_price DECIMAL,
product_description TEXT,
quantity INT
);
-- Проблемы:
-- 1. Дублирование: одного покупателя повторяем много раз
-- 2. Аномалии обновления: изменили email, нужно обновить везде
-- 3. Аномалии удаления: удалили последний заказ = удалили клиента
-- 4. Аномалии вставки: нельзя добавить клиента без заказа
Решение: нормализация
1NF (Первая нормальная форма)
- Каждая ячейка содержит одно значение (не список)
- Нет повторяющихся столбцов
-- Плохо: несколько значений в одной ячейке
CREATE TABLE products_bad (
id INT PRIMARY KEY,
name VARCHAR,
tags VARCHAR -- 'python,django,web' — несколько значений
);
-- Хорошо: каждое значение отдельно
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR
);
CREATE TABLE product_tags (
product_id INT REFERENCES products(id),
tag VARCHAR
);
2NF (Вторая нормальная форма)
- Соответствует 1NF
- Все неключевые атрибуты зависят от ВСЕГО первичного ключа
- Нет частичных зависимостей
-- Плохо: price зависит только от product_id, а не от (order_id, product_id)
CREATE TABLE order_items_bad (
order_id INT,
product_id INT,
quantity INT,
price DECIMAL, -- Зависит только от product_id!
PRIMARY KEY (order_id, product_id)
);
-- Хорошо: выносим цену в отдельную таблицу
CREATE TABLE products (
id INT PRIMARY KEY,
price DECIMAL
);
CREATE TABLE order_items (
order_id INT,
product_id INT REFERENCES products(id),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
3NF (Третья нормальная форма)
- Соответствует 2NF
- Нет зависимостей между неключевыми атрибутами
- Нет транзитивных зависимостей
-- Плохо: city зависит от country, хотя оба не ключи
CREATE TABLE customers_bad (
id INT PRIMARY KEY,
name VARCHAR,
country VARCHAR,
country_code VARCHAR -- Зависит от country!
);
-- Хорошо: выносим в отдельную таблицу
CREATE TABLE countries (
code VARCHAR PRIMARY KEY,
name VARCHAR
);
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR,
country_code VARCHAR REFERENCES countries(code)
);
Пример: нормализация системы заказов
ДО: Денормализованная (плохо)
import pandas as pd
# Вся информация в одной таблице
orders_data = [
{
'order_id': 1,
'order_date': '2024-01-01',
'customer_name': 'Alice',
'customer_email': 'alice@example.com',
'product_name': 'Laptop',
'product_price': 1000,
'quantity': 1
},
{
'order_id': 2,
'order_date': '2024-01-02',
'customer_name': 'Alice', # Дублирование!
'customer_email': 'alice@example.com',
'product_name': 'Mouse',
'product_price': 20,
'quantity': 2
}
]
df = pd.DataFrame(orders_data)
# Проблема: обновить email клиента нужно в обеих строках
df.loc[df['customer_name'] == 'Alice', 'customer_email'] = 'alice.new@example.com'
ПОСЛЕ: Нормализованная (хорошо)
from datetime import datetime
from typing import Optional
# Таблица 1: Клиенты
class Customer:
def __init__(self, id: int, name: str, email: str):
self.id = id
self.name = name
self.email = email
# Таблица 2: Продукты
class Product:
def __init__(self, id: int, name: str, price: float):
self.id = id
self.name = name
self.price = price
# Таблица 3: Заказы (содержит только ID)
class Order:
def __init__(self, id: int, customer_id: int, order_date: datetime):
self.id = id
self.customer_id = customer_id
self.order_date = order_date
# Таблица 4: Позиции заказа (связь много-ко-многим)
class OrderItem:
def __init__(self, order_id: int, product_id: int, quantity: int):
self.order_id = order_id
self.product_id = product_id
self.quantity = quantity
# SQL для создания нормализованной схемы
sql = """
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
email VARCHAR UNIQUE NOT NULL
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
price DECIMAL NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT NOT NULL REFERENCES customers(id),
order_date TIMESTAMP NOT NULL
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(id),
product_id INT NOT NULL REFERENCES products(id),
quantity INT NOT NULL,
UNIQUE(order_id, product_id)
);
-- Индексы для быстрых поиков
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_order_items_order ON order_items(order_id);
CREATE INDEX idx_order_items_product ON order_items(product_id);
"""
Преимущества нормализации
- Отсутствие дублирования: каждый факт хранится один раз
- Легкость обновлений: изменяем данные в одном месте
- Целостность: внешние ключи предотвращают ошибки
- Экономия памяти: нет повторяющихся данных
-- Обновить email — одна операция
UPDATE customers SET email = 'alice.new@example.com' WHERE id = 1;
-- Все заказы этого клиента автоматически обновлены через связь
SELECT o.* FROM orders o
WHERE o.customer_id = 1;
Когда денормализовать
Иногда нужна денормализация для производительности:
-- Денормализация: кэшируем count
ALTER TABLE customers ADD COLUMN order_count INT DEFAULT 0;
-- Это ускорит запросы типа "Найти клиентов с > 5 заказов"
SELECT * FROM customers WHERE order_count > 5;
-- Но нужно следить за синхронизацией (триггеры, обновления)
CREATE TRIGGER update_order_count AFTER INSERT ON orders
FOR EACH ROW
UPDATE customers SET order_count = order_count + 1 WHERE id = NEW.customer_id;
Практический совет
# Рекомендация: нормализовать в 3NF по умолчанию
# Денормализовать только если:
# 1. Есть ДОКАЗАННАЯ проблема производительности
# 2. Индексы и оптимизация не решили проблему
# 3. Готов поддерживать синхронизацию данных
# Используй ORM, который управляет связями:
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class Customer(Base):
__tablename__ = 'customers'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String, unique=True)
orders = relationship('Order', back_populates='customer')
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
customer_id = Column(Integer, ForeignKey('customers.id'))
customer = relationship('Customer', back_populates='orders')
items = relationship('OrderItem', back_populates='order')
Вывод
Нормализация — это основа хорошей архитектуры БД. Начни с 3NF, а потом денормализуй только где это реально нужно для производительности.