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

В чем смысл нормализации данных?

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);
"""

Преимущества нормализации

  1. Отсутствие дублирования: каждый факт хранится один раз
  2. Легкость обновлений: изменяем данные в одном месте
  3. Целостность: внешние ключи предотвращают ошибки
  4. Экономия памяти: нет повторяющихся данных
-- Обновить 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, а потом денормализуй только где это реально нужно для производительности.

В чем смысл нормализации данных? | PrepBro