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

Какими способами достигается нормализация базы данных?

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

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

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

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

Нормализация базы данных: методы и практика

Нормализация - это процесс организации данных в реляционной БД для уменьшения избыточности и зависимостей. Это фундаментальный навык проектировщика БД.

Почему нормализация важна

# Денормализованная таблица (плохо)
class Users:
    id
    username
    email
    phone
    street_address
    city
    state
    zip_code
    country
    post1_title
    post1_content
    post1_date
    post2_title
    post2_content
    post2_date
    post3_title
    post3_content
    post3_date
    # ... post4, post5, ... post100

# Проблемы:
# - Огромная таблица
# - Много NULL значений
# - Изменить адрес = обновить все записи
# - Добавить пост = добавить столбец ко ВСЕЙ таблице
# - Аномалии при удалении

# Нормализованная схема (хорошо)
class Users:          # 1NF
    id (PK)
    username
    email

class Addresses:      # 2NF
    id (PK)
    user_id (FK)
    street
    city
    state
    zip_code
    country

class Posts:          # 3NF
    id (PK)
    user_id (FK)
    title
    content
    created_at

# Преимущества:
# - Меньше данных
# - Можно изменить адрес в одном месте
# - Динамически добавлять посты
# - Нет аномалий
# - Лучше query performance (с индексами)

Нормальные формы (Normal Forms)

1NF (First Normal Form) - Атомарность

Правило: Каждое поле должно содержать только атомарные (неделимые) значения.

# ❌ Нарушает 1NF
users = """
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(100),
    hobbies VARCHAR(500),  -- 'reading, coding, gaming'
    tags VARCHAR(500)      -- 'python, java, golang'
);
"""

# Когда нужно найти всех, кто имеет hobby 'coding'
# SELECT * FROM users WHERE hobbies LIKE '%coding%'
# - Медленно (LIKE сканирует всю таблицу)
# - Неточно (найдет 'decoding' и 'encoding')

# ✓ Соответствует 1NF
users = """
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(100)
);

CREATE TABLE user_hobbies (
    id INT PRIMARY KEY,
    user_id INT REFERENCES users(id),
    hobby VARCHAR(100)
);

CREATE TABLE user_skills (
    id INT PRIMARY KEY,
    user_id INT REFERENCES users(id),
    skill VARCHAR(100)
);
"""

# Теперь:
query = """
SELECT DISTINCT u.* FROM users u
JOIN user_hobbies uh ON u.id = uh.user_id
WHERE uh.hobby = 'coding'
"""
# - Быстро (индекс на hobby)
# - Точно (только 'coding')

Как достичь 1NF:

  • Убрать multi-valued атрибуты
  • Создать отдельные таблицы для collections
  • Каждое значение в отдельной строке или отдельной таблице

2NF (Second Normal Form) - Зависимость от ключа

Правило: Должна быть 1NF + каждый non-key атрибут зависит от ВСЕГО первичного ключа (не от его части).

# ❌ Нарушает 2NF (composite key example)
orders = """
CREATE TABLE orders (
    order_id INT,
    product_id INT,
    customer_name VARCHAR(100),    -- Зависит от order_id, но не от product_id
    customer_email VARCHAR(100),   -- Зависит от order_id, но не от product_id
    product_name VARCHAR(100),     -- Зависит от product_id, но не от order_id
    quantity INT,                  -- Зависит от обоих (это OK)
    PRIMARY KEY (order_id, product_id)
);
"""

# Проблема: Anomaly
# Если изменить customer_name для order_id=1 в одной строке,
# но забыть обновить в другой (product_id=2) - inconsistency

# ✓ Соответствует 2NF
orders = """
CREATE TABLE orders (
    id INT PRIMARY KEY,
    customer_id INT REFERENCES customers(id),
    order_date DATE
);

CREATE TABLE customers (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

CREATE TABLE order_items (
    id INT PRIMARY KEY,
    order_id INT REFERENCES orders(id),
    product_id INT REFERENCES products(id),
    quantity INT
);

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);
"""

Как достичь 2NF:

  • Убедиться в 1NF
  • Для каждого non-key атрибута проверить: зависит ли от ВСЕ частей первичного ключа?
  • Если нет - перенести в отдельную таблицу

3NF (Third Normal Form) - Трансзитивная зависимость

Правило: Должна быть 2NF + нет трансзитивных зависимостей между non-key атрибутами.

# ❌ Нарушает 3NF
employees = """
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    department_id INT,
    department_name VARCHAR(100),  -- Зависит от department_id, не от id
    department_budget INT          -- Зависит от department_id, не от id
);
"""

# Проблема: Anomaly
# UPDATE employees SET department_name = 'Engineering' WHERE department_id = 5
# Но если забыть обновить budget - inconsistency

# ✓ Соответствует 3NF
employees = """
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    department_id INT REFERENCES departments(id)
);

CREATE TABLE departments (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    budget DECIMAL(10, 2)
);
"""

# Теперь department данные в одном месте

Как достичь 3NF:

  • Убедиться в 2NF
  • Для каждого non-key атрибута проверить: есть ли путь зависимостей через другие non-key атрибуты?
  • Если есть - создать отдельную таблицу

BCNF (Boyce-Codd Normal Form) - Строже чем 3NF

Правило: Для каждой зависимости X → Y, X должен быть candidate key.

# ❌ Нарушает BCNF (пример преподавателей и курсов)
teacher_courses = """
CREATE TABLE teacher_courses (
    teacher_id INT,
    course_id INT,
    time_slot INT,
    PRIMARY KEY (teacher_id, course_id)
);
"""

# Зависимость: (course_id, time_slot) → teacher_id
# Это значит: Один курс в один time_slot преподает один учитель
# Но (course_id, time_slot) не является candidate key
# Нарушение BCNF

# ✓ Соответствует BCNF
teacher_courses = """
CREATE TABLE course_schedule (
    id INT PRIMARY KEY,
    course_id INT REFERENCES courses(id),
    time_slot INT,
    teacher_id INT REFERENCES teachers(id),
    UNIQUE (course_id, time_slot)  -- (course_id, time_slot) - candidate key
);
"""

Денормализация (когда нужна)

# Нормализация улучшает update/insert, но может замедлить select
# Если у вас МНОГО читающих операций - может понадобиться денормализация

class DenormalizationStrategies:
    
    # 1. Computed columns
    def strategy_1(self):
        return """
        CREATE TABLE users (
            id INT PRIMARY KEY,
            username VARCHAR(100),
            post_count INT,  -- Денормализовано (вычисляется из posts)
            comment_count INT
        );
        
        -- Обновляется триггером
        CREATE TRIGGER update_post_count
        AFTER INSERT ON posts
        FOR EACH ROW
        BEGIN
            UPDATE users SET post_count = post_count + 1
            WHERE id = NEW.user_id;
        END;
        """
    
    # 2. Materialized views
    def strategy_2(self):
        return """
        -- Нормализованное представление
        CREATE TABLE user_statistics AS
        SELECT 
            u.id,
            u.username,
            COUNT(DISTINCT p.id) as post_count,
            COUNT(DISTINCT c.id) as comment_count,
            AVG(p.views) as avg_views
        FROM users u
        LEFT JOIN posts p ON u.id = p.user_id
        LEFT JOIN comments c ON p.id = c.post_id
        GROUP BY u.id;
        
        -- Обновляем каждый час
        REFRESH MATERIALIZED VIEW user_statistics;
        """
    
    # 3. Cache (Redis)
    def strategy_3(self):
        return """
        # Прочитали user stats из БД
        stats = db.query("SELECT ... FROM user_statistics WHERE user_id = ?", user_id)
        # Кэшируем на 1 час
        cache.setex(f'user_stats:{user_id}', 3600, json.dumps(stats))
        """

# Правило: Денормализуй ДЛЯ PERFORMANCE, но проверь что это нужно!
import time

def benchmark():
    start = time.time()
    for i in range(10000):
        # Нормализованная схема с JOIN
        result = db.execute(
            "SELECT COUNT(*) FROM posts WHERE user_id = ?", user_id
        )
    normalized_time = time.time() - start
    
    start = time.time()
    for i in range(10000):
        # Денормализованная схема (direct column)
        result = db.execute(
            "SELECT post_count FROM users WHERE id = ?", user_id
        )
    denormalized_time = time.time() - start
    
    if normalized_time > denormalized_time * 10:
        print(f"Денормализация дает {normalized_time/denormalized_time:.1f}x speedup")
        print(f"Worth it!")

Процесс нормализации (пошагово)

class NormalizationProcess:
    
    def step1_identify_entities(self):
        # Какие основные сущности?
        # Users, Posts, Comments, Likes
        return ["users", "posts", "comments", "likes"]
    
    def step2_identify_attributes(self):
        # Какие атрибуты у каждой сущности?
        return {
            "users": ["id", "username", "email", "created_at"],
            "posts": ["id", "user_id", "title", "content", "created_at"],
            "comments": ["id", "post_id", "user_id", "text", "created_at"],
            "likes": ["id", "post_id", "user_id", "created_at"],
        }
    
    def step3_identify_primary_keys(self):
        # Каждая сущность должна иметь primary key
        return {
            "users": "id",
            "posts": "id",
            "comments": "id",
            "likes": "id",
        }
    
    def step4_check_1nf(self):
        # Все значения атомарные? Нет arrays/lists?
        return True  # Это case
    
    def step5_check_2nf(self):
        # Все non-key атрибуты зависят от ВСЕ частей PK?
        # Если composite key - проверить
        return True
    
    def step6_check_3nf(self):
        # Есть ли трансзитивные зависимости?
        # Например: post.author_name (зависит от author_id, не от post_id)
        return True  # Уже отделили в отдельную таблицу
    
    def step7_identify_foreign_keys(self):
        # Какие relationships?
        return {
            "posts.user_id": "FK to users.id",
            "comments.post_id": "FK to posts.id",
            "comments.user_id": "FK to users.id",
            "likes.post_id": "FK to posts.id",
            "likes.user_id": "FK to users.id",
        }
    
    def step8_create_indexes(self):
        # Какие индексы помогут performance?
        return [
            "CREATE INDEX idx_posts_user_id ON posts(user_id)",
            "CREATE INDEX idx_comments_post_id ON comments(post_id)",
            "CREATE INDEX idx_comments_user_id ON comments(user_id)",
            "CREATE INDEX idx_likes_post_id ON likes(post_id)",
        ]

Реальный пример: Проектирование schema

-- Хороший, нормализованный дизайн

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(100) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE posts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title VARCHAR(255) NOT NULL,
    content LONGTEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_created_at (created_at)
);

CREATE TABLE comments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    text TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_post_id (post_id),
    INDEX idx_user_id (user_id),
    INDEX idx_created_at (created_at)
);

CREATE TABLE likes (
    id INT PRIMARY KEY AUTO_INCREMENT,
    post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY unique_post_user (post_id, user_id),
    INDEX idx_user_id (user_id)
);

Инструменты для проверки нормализации

# 1. DrawSQL.app - визуально спроектировать
# 2. DBML - язык для описания БД
# 3. SQL лinter - проверить на ошибки
# 4. Explain analyze - проверить производительность
# 5. Code review процесс - другой разработчик проверит design

import sqlparse

def analyze_schema_normalization(schema):
    # Simple checks
    issues = []
    
    # Проверка 1: Есть ли PRIMARY KEY в каждой таблице?
    if "PRIMARY KEY" not in schema:
        issues.append("Missing PRIMARY KEY")
    
    # Проверка 2: Есть ли повторяющиеся комбинации?
    if "hobbies VARCHAR(500)" in schema:
        issues.append("Violates 1NF: multi-valued attribute")
    
    # Проверка 3: Есть ли redundant данные?
    if "department_name" in schema and "department_id" in schema:
        issues.append("Possible 3NF violation: department details should be separate")
    
    return issues

Итоги

Нормальная формаПравилоПроверка
1NFАтомарные значенияНет lists/arrays в колонках
2NFЗависимость от ВСЕ PKNon-key атрибуты зависят от целого PK
3NFНет трансзитивных зависимостейNon-key атрибуты не зависят друг от друга
BCNFДетерминант = candidate keyБолее строгая чем 3NF

Мой совет: Начните с 3NF - это хороший баланс между нормализацией и производительностью. Денормализируйте ТОЛЬКО если измеренная производительность это требует.