🧚 Ei! O Beekeeper Studio é uma GUI de banco de dados rápida, moderna e open source Baixar
2 de maio de 2026 Por Matthew Rathbone

Um relacionamento muitos-para-muitos liga duas tabelas em que uma linha de qualquer lado pode corresponder a muitas linhas do outro: um aluno faz vários cursos, e um curso tem vários alunos. Você o modela com uma tabela de junção que guarda uma linha por vínculo. Os exemplos funcionam no PostgreSQL, MySQL, SQLite e SQL Server.

Como modelar um relacionamento muitos-para-muitos

Crie uma terceira tabela, a tabela de junção (também chamada de tabela ponte, de associação ou join table), com uma chave estrangeira para cada lado. Cada linha é um vínculo: este aluno faz este curso.

Diagrama de relacionamento muitos-para-muitos em banco de dados

CREATE TABLE students (
    id INTEGER PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE courses (
    id INTEGER PRIMARY KEY,
    course_name VARCHAR(100) NOT NULL
);

-- The junction table: one row per student per course
CREATE TABLE enrollments (
    student_id INTEGER NOT NULL,
    course_id INTEGER NOT NULL,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

Para ir de um lado ao outro, faça o join passando pela tabela de junção:

-- Input: students
-- id | name
-- ---+------
-- 1  | Ana
-- 2  | Ben
-- 3  | Chloe

-- Input: courses
-- id | course_name
-- ---+------------
-- 1  | Biology
-- 2  | Chemistry
-- 3  | Physics

-- Input: enrollments
-- student_id | course_id
-- -----------+----------
-- 1          | 1
-- 1          | 2
-- 2          | 1
-- 3          | 1

SELECT s.name, c.course_name
FROM students s
JOIN enrollments e ON s.id = e.student_id
JOIN courses c ON e.course_id = c.id
ORDER BY s.name, c.course_name;

-- Output:
-- name  | course_name
-- ------+------------
-- Ana   | Biology
-- Ana   | Chemistry
-- Ben   | Biology
-- Chloe | Biology

Ana faz dois cursos e Biologia tem três alunos, então o relacionamento é “muitos” nas duas direções.

Regras importantes

  • Use o par como chave primária. PRIMARY KEY (student_id, course_id) impede que o mesmo vínculo seja gravado duas vezes:
-- Input: the enrollments table above

INSERT INTO enrollments VALUES (1, 1);
-- ERROR: duplicate key value violates unique constraint "enrollments_pkey"
  • Indexe a segunda coluna. O índice da chave primária começa com student_id, então já encontra rapidamente os cursos de um aluno. Para encontrar rapidamente os alunos de um curso, é preciso um índice que comece com course_id (o MySQL cria um para você junto com a chave estrangeira):
CREATE INDEX idx_enrollments_course_id ON enrollments(course_id);
  • Guarde uma linha por vínculo, nunca uma lista. Uma coluna course_ids contendo '1,2' não pode ser usada em joins, indexada nem validada por uma chave estrangeira.
  • O SQLite ignora chaves estrangeiras até você ativá-las. Execute PRAGMA foreign_keys = ON; em cada conexão, ou o SQLite vai aceitar uma matrícula de um aluno que não existe.

Sintaxe alternativa

Alguns frameworks dão a toda tabela sua própria coluna id, incluindo as tabelas de junção. Isso funciona desde que você adicione uma restrição UNIQUE no par, porque o id sozinho não impede vínculos duplicados. Só a coluna id muda entre os bancos de dados:

-- PostgreSQL
CREATE TABLE enrollments (
    id SERIAL PRIMARY KEY,
    student_id INTEGER NOT NULL,
    course_id INTEGER NOT NULL,
    UNIQUE (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id) REFERENCES courses(id)
);

-- MySQL:      id INT AUTO_INCREMENT PRIMARY KEY
-- SQLite:     id INTEGER PRIMARY KEY
-- SQL Server: id INT IDENTITY PRIMARY KEY

Você também pode declarar uma chave estrangeira inline: student_id INTEGER NOT NULL REFERENCES students(id). O MySQL 8.0 aceita essa forma, mas a ignora silenciosamente, então mantenha a cláusula FOREIGN KEY separada nele.

Quando usar um relacionamento muitos-para-muitos?

Use sempre que as linhas dos dois lados puderem ter vários parceiros: alunos e cursos, posts e tags, usuários e papéis, produtos e categorias. Se cada linha de um lado tiver sempre um único parceiro, um relacionamento um-para-muitos é suficiente. Com a tabela de junção pronta, ela resolve estes problemas.

Consultar a que uma linha está vinculada

Quais cursos um aluno faz? Quais tags estão em um post? Faça o join da linha para a tabela de junção e dela para o outro lado. A mesma tabela de junção responde à pergunta nas duas direções.

O curso mais cheio, a tag mais usada, o membro com mais clubes: agrupe por um lado e conte suas linhas na tabela de junção. Use um LEFT JOIN quando linhas sem vínculos devem contar como 0 em vez de sumir.

Uma nota pertence a um aluno em um curso, não só ao aluno ou só ao curso, e uma participação nos royalties pertence a um autor em um livro específico. Coloque essas colunas na tabela de junção e consulte-as como qualquer outra coluna.

Seguidores, amigos e mentores ligam pessoas a outras pessoas. As duas chaves estrangeiras da tabela de junção apontam para a mesma tabela, então você faz o join dessa tabela duas vezes, com um alias diferente em cada uma.

Exemplos reais de relacionamentos muitos-para-muitos

Encontrar posts que têm todas as tags de uma lista

Um filtro de tags geralmente significa “posts com todas estas tags”. Busque qualquer uma das tags e depois mantenha os posts que corresponderam a todas:

-- Input: posts
-- id | title
-- ---+----------------
-- 1  | Indexing basics
-- 2  | Weekend hiking
-- 3  | Joins explained

-- Input: tags
-- id | name
-- ---+---------
-- 1  | sql
-- 2  | outdoors
-- 3  | beginner

-- Input: post_tags
-- post_id | tag_id
-- --------+-------
-- 1       | 1
-- 1       | 3
-- 2       | 2
-- 2       | 3
-- 3       | 1

SELECT p.title
FROM posts p
JOIN post_tags pt ON p.id = pt.post_id
JOIN tags t ON pt.tag_id = t.id
WHERE t.name IN ('sql', 'beginner')
GROUP BY p.id, p.title
HAVING COUNT(*) = 2;

-- Output:
-- title
-- ---------------
-- Indexing basics

“Joins explained” tem só uma das duas tags, então é descartado no HAVING. Troque o 2 pelo número de tags da lista.

Conte as matrículas de cada curso. O LEFT JOIN mantém Física, em que ninguém se inscreveu:

-- Input: courses
-- id | course_name
-- ---+------------
-- 1  | Biology
-- 2  | Chemistry
-- 3  | Physics

-- Input: enrollments
-- student_id | course_id
-- -----------+----------
-- 1          | 1
-- 1          | 2
-- 2          | 1
-- 3          | 1

SELECT c.course_name, COUNT(e.student_id) AS student_count
FROM courses c
LEFT JOIN enrollments e ON c.id = e.course_id
GROUP BY c.id, c.course_name
ORDER BY student_count DESC;

-- Output:
-- course_name | student_count
-- ------------+--------------
-- Biology     | 3
-- Chemistry   | 1
-- Physics     | 0

Registrar quem trabalhou onde e quando

Dê a cada vínculo uma data de início e de fim, e a tabela de junção passa a guardar um histórico. Alguém pode voltar a um departamento que deixou, então o par deixa de ser único e a data de início entra na chave primária. O departamento atual é a linha sem data de fim:

CREATE TABLE employee_departments (
    employee_id INTEGER NOT NULL,
    department_id INTEGER NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE,  -- NULL while it's their current department
    PRIMARY KEY (employee_id, department_id, start_date),
    FOREIGN KEY (employee_id) REFERENCES employees(id),
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

-- Input: employees
-- id | name
-- ---+------
-- 1  | Grace
-- 2  | Omar
-- 3  | Wei

-- Input: departments
-- id | name
-- ---+------------
-- 1  | Sales
-- 2  | Support
-- 3  | Engineering

-- Input: employee_departments
-- employee_id | department_id | start_date | end_date
-- ------------+---------------+------------+-----------
-- 1           | 1             | 2022-03-01 | 2024-06-30
-- 1           | 3             | 2024-07-01 | NULL
-- 2           | 2             | 2023-01-09 | NULL
-- 3           | 1             | 2021-05-17 | NULL

SELECT e.name, d.name AS department, ed.start_date
FROM employees e
JOIN employee_departments ed ON e.id = ed.employee_id
JOIN departments d ON ed.department_id = d.id
WHERE ed.end_date IS NULL
ORDER BY e.name;

-- Output:
-- name  | department  | start_date
-- ------+-------------+-----------
-- Grace | Engineering | 2024-07-01
-- Omar  | Support     | 2023-01-09
-- Wei   | Sales       | 2021-05-17

Para impedir que alguém tenha dois departamentos atuais, adicione um índice único parcial: CREATE UNIQUE INDEX idx_current_dept ON employee_departments(employee_id) WHERE end_date IS NULL;. O MySQL não tem índices parciais, então isso não funciona nele.

Encontrar usuários que se seguem mutuamente

Uma tabela follows liga usuários a usuários. Uma restrição CHECK impede que alguém siga a si mesmo, e o join da tabela com ela mesma encontra os pares que se seguem mutuamente:

CREATE TABLE follows (
    follower_id INTEGER NOT NULL,
    followed_id INTEGER NOT NULL,
    PRIMARY KEY (follower_id, followed_id),
    FOREIGN KEY (follower_id) REFERENCES users(id),
    FOREIGN KEY (followed_id) REFERENCES users(id),
    CONSTRAINT no_self_follow CHECK (follower_id <> followed_id)
);

-- INSERT INTO follows VALUES (2, 2);
-- ERROR: new row for relation "follows" violates check constraint "no_self_follow"

-- Input: users
-- id | name
-- ---+-----
-- 1  | Ava
-- 2  | Leo
-- 3  | Mia
-- 4  | Noah

-- Input: follows
-- follower_id | followed_id
-- ------------+------------
-- 1           | 2
-- 2           | 1
-- 1           | 3
-- 3           | 4
-- 4           | 3

SELECT u1.name AS user_a, u2.name AS user_b
FROM follows f
JOIN follows back ON back.follower_id = f.followed_id
                 AND back.followed_id = f.follower_id
JOIN users u1 ON u1.id = f.follower_id
JOIN users u2 ON u2.id = f.followed_id
WHERE f.follower_id < f.followed_id
ORDER BY user_a;

-- Output:
-- user_a | user_b
-- -------+-------
-- Ava    | Leo
-- Mia    | Noah

Ava segue Mia, mas Mia não a segue de volta, então esse par não aparece. A condição f.follower_id < f.followed_id lista cada par mútuo uma vez em vez de duas.

Leituras adicionais sobre relacionamentos muitos-para-muitos

Neste blog:

Documentação oficial:

Beekeeper Studio é um cliente de banco de dados gratuito e open source

A melhor ferramenta de consultas e edição de SQL que já usei. Tem tudo o que preciso para gerenciar meu banco de dados. - ⭐⭐⭐⭐⭐ Mit

O Beekeeper Studio é rápido, intuitivo e fácil de usar. Ele suporta vários bancos de dados e funciona muito bem no Windows, Mac e Linux.

A versão do Beekeeper para Linux tem 100% dos recursos, sem atalhos e sem concessões.

O que os usuários dizem sobre o Beekeeper Studio

★★★★★
"O Beekeeper Studio substituiu completamente meu antigo fluxo de trabalho com SQL. É rápido, intuitivo e torna o trabalho com banco de dados prazeroso de novo."
— Alex K., Desenvolvedor de banco de dados
★★★★★
"Já testei muitas GUIs de banco de dados, mas o Beekeeper encontra o equilíbrio perfeito entre recursos e simplicidade. Simplesmente funciona."
— Sarah M., Engenheira Full Stack

Pronto para melhorar seu fluxo de trabalho com SQL?

download Baixar grátis