Many-to-many relationship
Both sides can have several of the other, so a middle table stores each link, like memberships between users and teams.
See it
What it is
A many-to-many relationship means rows on both sides can relate to several rows on the other: users join many teams and a team has many users. Relational databases represent it with a junction table such as memberships, with one foreign key to users and one to teams. Each row in the junction is one connection.
Reach for it when the connection is a real set rather than one side owning the other. The junction can carry facts about that connection, such as a member's role, joined_at date, or notification setting. At that point it is often useful to think of Membership as its own model, not invisible plumbing. Frameworks encode the same lesson: Rails teams reach for has_many :through over the older has_and_belongs_to_many precisely because the first one gives the link a real model, and Django's ManyToManyField hides its through table until you need to add a column and have to declare it anyway.
Gotcha: two foreign keys do not stop the same pair being inserted twice. Make (user_id, team_id) the composite primary key, or add a unique constraint on that pair if the table has a separate id. Indexing the reverse order can also matter: the pair starting with user_id finds a user's teams, but a second index starting with team_id helps find a team's users.
Ask AI for it
Model this PostgreSQL many-to-many relationship with an explicit junction table. Add NOT NULL foreign keys to both parent tables, make the pair a composite PRIMARY KEY, and add relationship attributes such as role or created_at only when they belong to the link itself. Choose each ON DELETE action explicitly, create a reverse-order B-tree index when both lookup directions are used, and do not store arrays of ids on either parent. Output the CREATE TABLE and CREATE INDEX statements plus queries for both directions and a test proving the same pair cannot be inserted twice.