FigSchema logoInstall free

Many-to-many relationships and junction tables, explained simply

A foreign key is a single column, and a column lives in exactly one table. That's why one-to-many is easy: orders.user_id points at users.id, done. But "a student takes many courses, and every course has many students" doesn't fit in any column. SQL has no courses[].

Never draw many-to-many directly

If a line in your diagram has a crow's foot on both ends, it can't become a foreign key - the diagram is telling you it's unimplementable as drawn. The fix is always the same: put a third table in the middle and split the relationship into two plain one-to-many links.

A many-to-many relationship drawn incorrectly, then fixed with an enrollments junction table between students and courses

That middle table - enrollments - is the junction table (you'll also hear linking table, join table, association table). Its whole job is to hold pairs of foreign keys.

What the rows actually look like

This is where it clicks for most people: a junction table is just one row per pairing.

Sample rows of a bare post_tags junction table next to an enrollments table carrying grade and enrolled_at payload columns

  • Bare version (post_tags): each row is one pairing, and the pair itself is usually the primary key.
  • Loaded version (enrollments): same pairs, plus data that belongs to the link - not to either side.

Payload: the link itself has data

A grade doesn't belong to the student (they have many grades) or to the course (many students earn different grades). It belongs to the pairing. Same story with qty on an order line, or assigned_at on a role assignment. Once you treat the junction table as a real entity with its own columns - not a hack - you'll start noticing data parked on the wrong table everywhere.

Four junction tables you'll actually build

Junction tableConnectsPayload columns
enrollmentsstudents ↔ coursesgrade, semester
order_itemsorders ↔ productsqty, unit_price
post_tagsposts ↔ tagsnone - bare pairs
role_assignmentsusers ↔ rolesassigned_at, assigned_by

How to spot it while sketching

Three tells, in the order they usually show up:

  1. A requirement sentence with "each … many … and each … many …" - students and courses, posts and tags.
  2. You catch yourself wishing for an array column (tag_ids on posts). Arrays of ids are the runtime hack; the junction table is the schema answer.
  3. A line in your diagram with feet on both ends - the notation itself is the alarm.

When you hit one, don't redraw the two tables - drop a third in the middle, exactly like step five of the FigJam walkthrough.

Naming the middle table

If the pairing is a real-world thing, name it that: enrollments, order_items, subscriptions. If it's just a pairing with no natural name, table1_table2 (post_tags) is the common fallback. Skip suffixes like _map or _link - they're noise.

Sketching this in FigJam? FigSchema anchors every link to the exact columns it connects - so when a relationship turns out many-to-many, dropping the junction table in the middle keeps both halves of the diagram truthful. Free, offline, no account.