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

Что такое нормальная форма?

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

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

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

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

# Нормальная форма в базах данных

Нормальная форма — это набор правил проектирования БД, которые гарантируют минимальное дублирование данных и логическую непротиворечивость. Нарушение этих правил приводит к аномалиям при обновлении, удалении и вставке данных.

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

Представьте таблицу студентов с их курсами:

ID | Name   | Courses
---|--------|------------------------
1  | Alice  | Python, SQL, Django
2  | Bob    | Python, React

Проблемы:

  • Аномалия обновления: если нужно изменить название курса "Python" -> "Python 3", нужно обновить все строки
  • Аномалия удаления: если удалить Alice, потеряем информацию о курсах
  • Аномалия вставки: не можем добавить курс, если нет студента

1НФ (First Normal Form)

Правило: Каждое поле содержит только одно значение (нет повторяющихся групп).

Нарушение 1НФ:

ID | Name   | Courses
---|--------|------------------------
1  | Alice  | Python, SQL, Django  <- список в одном поле

В 1НФ:

ID | Name   | Course
---|--------|--------
1  | Alice  | Python
1  | Alice  | SQL
1  | Alice  | Django
2  | Bob    | Python

2НФ (Second Normal Form)

Правило: Таблица в 1НФ, и все неключевые атрибуты полностью зависят от первичного ключа (нет частичной зависимости).

Нарушение 2НФ:

StudentID | StudentName | CourseID | CourseName | InstructorID
----------|-------------|----------|------------|---------------
1         | Alice       | 101      | Python     | 5
1         | Alice       | 102      | SQL        | 5

Проблема: CourseName зависит от CourseID, но не от полного ключа (StudentID, CourseID).

В 2НФ:

-- Students
ID | Name
---|------
1  | Alice
2  | Bob

-- Courses
ID | Name
---|--------
101| Python
102| SQL

-- Enrollments
StudentID | CourseID
----------|----------
1         | 101
1         | 102
2         | 101

3НФ (Third Normal Form)

Правило: Таблица в 2НФ, и нет транзитивной зависимости (неключевые атрибуты зависят от ключа, а не друг от друга).

Нарушение 3НФ:

StudentID | StudentName | DepartmentID | DepartmentName | DepartmentHead
-----------|-------------|--------------|----------------|----------------
1          | Alice       | 10           | Engineering    | Dr. Smith
2          | Bob         | 10           | Engineering    | Dr. Smith

Проблема: DepartmentName и DepartmentHead зависят от DepartmentID, который не является первичным ключом.

В 3НФ:

-- Students
ID | Name  | DepartmentID
---|-------|---------------
1  | Alice | 10
2  | Bob   | 10

-- Departments
ID | Name         | Head
---|--------------|----------
10 | Engineering  | Dr. Smith

BCNF (Boyce-Codd Normal Form)

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

Пример нарушения BCNF:

Professor | Course | Time
----------|--------|--------
Smith     | Math   | 10:00
Smith     | Math   | 11:00  <- Smith может вести один Math в разное время

4НФ (Fourth Normal Form)

Правило: Нет независимых многозначных зависимостей.

Таблица с двумя независимыми многозначными атрибутами:

Author | Topic  | Publisher
-------|--------|----------
Smith  | AI     | Springer
Smith  | AI     | Academic
Smith  | ML     | Springer
Smith  | ML     | Academic

В 4НФ разделить на две таблицы:

-- AuthorTopics
Author | Topic
-------|-------
Smith  | AI
Smith  | ML

-- AuthorPublishers
Author | Publisher
-------|----------
Smith  | Springer
Smith  | Academic

Практический пример на Python с SQLAlchemy

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, Session

Base = declarative_base()

class Student(Base):
    __tablename__ = 'students'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    # Нет field для courses (избегаем 1НФ нарушения)
    
    # Relationship вместо хранения списка
    enrollments = relationship('Enrollment', back_populates='student')

class Course(Base):
    __tablename__ = 'courses'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    instructor_id = Column(Integer, ForeignKey('instructors.id'))
    
    instructor = relationship('Instructor')
    enrollments = relationship('Enrollment', back_populates='course')

class Instructor(Base):
    __tablename__ = 'instructors'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    department_id = Column(Integer, ForeignKey('departments.id'))
    
    department = relationship('Department')

class Department(Base):
    __tablename__ = 'departments'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    head = Column(String(100))

class Enrollment(Base):
    __tablename__ = 'enrollments'
    
    student_id = Column(Integer, ForeignKey('students.id'), primary_key=True)
    course_id = Column(Integer, ForeignKey('courses.id'), primary_key=True)
    grade = Column(String(2))
    
    student = relationship('Student', back_populates='enrollments')
    course = relationship('Course', back_populates='enrollments')

# Использование
engine = create_engine('postgresql://user:pass@localhost/db')
Base.metadata.create_all(engine)

with Session(engine) as session:
    # Добавляем данные
    dept = Department(name='Engineering', head='Dr. Smith')
    session.add(dept)
    session.commit()
    
    instructor = Instructor(name='John', department_id=dept.id)
    session.add(instructor)
    session.commit()
    
    course = Course(name='Python', instructor_id=instructor.id)
    student = Student(name='Alice')
    session.add_all([course, student])
    session.commit()
    
    # Связываем через Enrollment
    enrollment = Enrollment(
        student_id=student.id,
        course_id=course.id,
        grade='A'
    )
    session.add(enrollment)
    session.commit()

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

Иногда нормализация приводит к множеству JOIN'ов, замедляя запросы:

# Нормализованный (медленный) запрос
from sqlalchemy import select

query = (
    select(Student.name, Course.name, Instructor.name, Department.name)
    .join(Enrollment)
    .join(Course)
    .join(Instructor)
    .join(Department)
    .where(Student.id == 1)
)

# Кеширование + денормализация
class StudentWithCache(Base):
    __tablename__ = 'students_cache'
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    courses_json = Column(JSON)  # Денормализованные данные
    updated_at = Column(DateTime, default=datetime.utcnow)

Правило большого пальца

  • До 3НФ — идеально для большинства приложений
  • BCNF/4НФ — редко требуется
  • Денормализация — когда производительность критична (кеш, аналитика)

Ключевой принцип: Нормализация минимизирует аномалии данных, но требует больше JOIN'ов при чтении. Выбирайте компромисс в зависимости от сценария.

Что такое нормальная форма? | PrepBro