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

Что такое Many-to-Many?

1.2 Junior🔥 91 комментариев
#SQL и базы данных

Комментарии (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

Важные замечания

  1. PRIMARY KEY в M2M: обычно составной из обеих FK

    PRIMARY KEY (student_id, course_id)
    
  2. Индексы: добавь индексы на FK для быстрых JOIN'ов

    CREATE INDEX idx_student ON student_courses(student_id);
    CREATE INDEX idx_course ON student_courses(course_id);
    
  3. Дополнительные данные: промежуточная таблица может иметь свои поля

    ALTER TABLE student_courses ADD grade VARCHAR(1);  -- A, B, C...
    ALTER TABLE student_courses ADD attended_date DATE;
    
  4. Нормализация: M2M — это нормальная форма (3NF)

    • Избегает дублирования данных
    • Улучшает целостность

Итоговая схема

Many-to-Many = 2 таблицы + 1 промежуточная таблица

Примечание: промежуточная таблица (junction table) критична для реализации M2M отношения. Без неё это One-to-Many или One-to-One.