Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Many-to-Many (M2M): связь многие-ко-многим
Что такое Many-to-Many в двух словах
Many-to-Many — это отношение между двумя таблицами, где:
- Одна запись таблицы A может быть связана со многими записями таблицы B
- И одна запись таблицы B может быть связана со многими записями таблицы A
Классический пример: Студенты и Курсы
Таблица: Students (студенты)
┌─────────┬────────┐
│ id │ name │
├─────────┼────────┤
│ 1 │ Alice │
│ 2 │ Bob │
│ 3 │ Charlie│
└─────────┴────────┘
↑
│ Many-to-Many (через junction table)
↓
Таблица: Courses (курсы)
┌─────────┬──────────────┐
│ id │ name │
├─────────┼──────────────┤
│ 1 │ Python │
│ 2 │ SQL │
│ 3 │ Web Dev │
└─────────┴──────────────┘
Визуализация:
Alice → Python
→ SQL
→ Web Dev
Bob → Python
→ SQL
Charlie → Web Dev
Видим: студент может быть на многих курсах, курс может иметь много студентов
SQL реализация
Нужна промежуточная таблица (junction table):
-- Основные таблицы
CREATE TABLE students (
student_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE courses (
course_id INT PRIMARY KEY,
name VARCHAR(100)
);
-- Промежуточная таблица (junction table, association table)
CREATE TABLE student_courses (
student_id INT NOT NULL,
course_id INT NOT NULL,
enrolled_date DATE,
PRIMARY KEY (student_id, course_id), -- составной первичный ключ
FOREIGN KEY (student_id) REFERENCES students(student_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
Данные в таблицах:
-- Студенты
INSERT INTO students VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Charlie');
-- Курсы
INSERT INTO courses VALUES
(1, 'Python'),
(2, 'SQL'),
(3, 'Web Dev');
-- Связи (Many-to-Many)
INSERT INTO student_courses (student_id, course_id, enrolled_date) VALUES
(1, 1, '2024-01-01'), -- Alice на Python
(1, 2, '2024-01-05'), -- Alice на SQL
(1, 3, '2024-01-10'), -- Alice на Web Dev
(2, 1, '2024-01-02'), -- Bob на Python
(2, 2, '2024-01-06'), -- Bob на SQL
(3, 3, '2024-01-08'); -- Charlie на Web Dev
Запросы M2M
Найти все курсы студента
SELECT c.course_id, c.name
FROM courses c
JOIN student_courses sc ON c.course_id = sc.course_id
WHERE sc.student_id = 1; -- Alice
-- Результат:
-- course_id | name
-- 1 | Python
-- 2 | SQL
-- 3 | Web Dev
Найти всех студентов на курсе
SELECT s.student_id, s.name
FROM students s
JOIN student_courses sc ON s.student_id = sc.student_id
WHERE sc.course_id = 1; -- Python
-- Результат:
-- student_id | name
-- 1 | Alice
-- 2 | Bob
Количество курсов на студента
SELECT
s.name,
COUNT(sc.course_id) as courses_count
FROM students s
LEFT JOIN student_courses sc ON s.student_id = sc.student_id
GROUP BY s.student_id, s.name
ORDER BY courses_count DESC;
-- Результат:
-- name | courses_count
-- Alice | 3
-- Bob | 2
-- Charlie | 1
Реальные примеры M2M
1. Пользователи и Теги (например в блоге)
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(100)
);
CREATE TABLE tags (
tag_id INT PRIMARY KEY,
tag_name VARCHAR(50)
);
CREATE TABLE user_tags (
user_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (user_id, tag_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
FOREIGN KEY (tag_id) REFERENCES tags(tag_id)
);
-- Пример: пользователь может иметь теги "python", "data", "ml"
2. Товары и Категории (e-commerce)
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(200)
);
CREATE TABLE categories (
category_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE product_categories (
product_id INT NOT NULL,
category_id INT NOT NULL,
PRIMARY KEY (product_id, category_id),
FOREIGN KEY (product_id) REFERENCES products(product_id),
FOREIGN KEY (category_id) REFERENCES categories(category_id)
);
-- Пример: ноутбук может быть в категориях "Электроника", "Компьютеры", "Подарки"
3. Актеры и Фильмы (IMDB like)
CREATE TABLE actors (
actor_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE movies (
movie_id INT PRIMARY KEY,
title VARCHAR(200)
);
CREATE TABLE movie_cast (
actor_id INT NOT NULL,
movie_id INT NOT NULL,
role VARCHAR(100), -- роль актера
PRIMARY KEY (actor_id, movie_id),
FOREIGN KEY (actor_id) REFERENCES actors(actor_id),
FOREIGN KEY (movie_id) REFERENCES movies(movie_id)
);
-- Пример: актер снимался в нескольких фильмах, каждый фильм имеет много актеров
4. Разрешения (Permissions) в системе
CREATE TABLE roles (
role_id INT PRIMARY KEY,
role_name VARCHAR(50) -- Admin, User, Guest
);
CREATE TABLE permissions (
permission_id INT PRIMARY KEY,
permission_name VARCHAR(100) -- read, write, delete
);
CREATE TABLE role_permissions (
role_id INT NOT NULL,
permission_id INT NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(role_id),
FOREIGN KEY (permission_id) REFERENCES permissions(permission_id)
);
-- Пример: роль Admin может иметь разрешения: read, write, delete
-- роль User может иметь: read, write
SQLAlchemy ORM: Many-to-Many
from sqlalchemy import Column, Integer, String, Table, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
# Промежуточная таблица
student_courses = Table(
'student_courses',
Base.metadata,
Column('student_id', Integer, ForeignKey('students.student_id')),
Column('course_id', Integer, ForeignKey('courses.course_id'))
)
class Student(Base):
__tablename__ = 'students'
student_id = Column(Integer, primary_key=True)
name = Column(String(100))
# Many-to-Many relationship
courses = relationship(
'Course',
secondary=student_courses,
back_populates='students'
)
class Course(Base):
__tablename__ = 'courses'
course_id = Column(Integer, primary_key=True)
name = Column(String(100))
# Обратная связь
students = relationship(
'Student',
secondary=student_courses,
back_populates='courses'
)
# Использование
from sqlalchemy.orm import Session
with Session(engine) as session:
# Получить студента
student = session.query(Student).filter_by(student_id=1).first()
# Все его курсы
for course in student.courses:
print(f"{student.name} → {course.name}")
# Добавить курс
course = session.query(Course).filter_by(course_id=1).first()
student.courses.append(course)
session.commit()
Отличие M2M от One-to-Many
One-to-Many (1:M) Many-to-Many (M:M)
Компания → Работники Студенты ↔ Курсы
(каждый работник (каждый студент
в одной компании) может на многих курсах)
┌────────────┐ ┌──────────┐
│ Companies │ │ Students │
├────────────┤ ├──────────┤
│ id │←FK────┐ │ id │
│ name │ │ │ name │
└────────────┘ │ └──────────┘
│ ↑↓
┌──────┴─────┐ ┌─────────────────┐
│ Employees │ │ Student_Courses │
├────────────┤ ├─────────────────┤
│ id │ │ student_id FK │
│ name │ │ course_id FK │
│ company_id │ └─────────────────┘
└────────────┘ ↑
│FK
┌─────────┐
│ Courses │
├─────────┤
│ id │
│ name │
└─────────┘
Важные отличия:
- One-to-Many: прямая связь через FK
- Many-to-Many: нужна промежуточная таблица
Проблемы и решения
Проблема 1: Дублирование в анализе
-- Если неправильно JOIN'ить, будет дублирование
SELECT s.*, sc.*, c.*
FROM students s
JOIN student_courses sc ON s.student_id = sc.student_id
JOIN courses c ON sc.course_id = c.course_id
WHERE s.student_id = 1;
-- Результат: 3 строки (одна на каждый курс)
-- Если используешь COUNT(*) - неправильно!
-- Используй: COUNT(DISTINCT c.course_id)
Проблема 2: Удаление связей
-- Удалить связь между студентом и курсом
DELETE FROM student_courses
WHERE student_id = 1 AND course_id = 1;
-- При этом сам студент и курс остаются в БД
-- Это отличие от CASCADE DELETE в One-to-Many
Анализ M2M данных
Топ курсы по популярности:
SELECT
c.name,
COUNT(sc.student_id) as students_count
FROM courses c
LEFT JOIN student_courses sc ON c.course_id = sc.course_id
GROUP BY c.course_id, c.name
ORDER BY students_count DESC;
Студенты, посещающие оба курса:
SELECT s.name
FROM students s
JOIN student_courses sc1 ON s.student_id = sc1.student_id
JOIN student_courses sc2 ON s.student_id = sc2.student_id
WHERE sc1.course_id = 1 -- Python
AND sc2.course_id = 2; -- SQL
Важные замечания
-
PRIMARY KEY в M2M: обычно составной из обеих FK
PRIMARY KEY (student_id, course_id) -
Индексы: добавь индексы на FK для быстрых JOIN'ов
CREATE INDEX idx_student ON student_courses(student_id); CREATE INDEX idx_course ON student_courses(course_id); -
Дополнительные данные: промежуточная таблица может иметь свои поля
ALTER TABLE student_courses ADD grade VARCHAR(1); -- A, B, C... ALTER TABLE student_courses ADD attended_date DATE; -
Нормализация: M2M — это нормальная форма (3NF)
- Избегает дублирования данных
- Улучшает целостность
Итоговая схема
Many-to-Many = 2 таблицы + 1 промежуточная таблица
Примечание: промежуточная таблица (junction table) критична для реализации M2M отношения. Без неё это One-to-Many или One-to-One.