Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Нормальная форма в базах данных
Нормальная форма — это набор правил проектирования БД, которые гарантируют минимальное дублирование данных и логическую непротиворечивость. Нарушение этих правил приводит к аномалиям при обновлении, удалении и вставке данных.
Зачем нужна нормализация
Представьте таблицу студентов с их курсами:
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'ов при чтении. Выбирайте компромисс в зависимости от сценария.