Unit 2 · Module 3

The Three Relationship Shapes

One-to-one, one-to-many, many-to-many — and where the foreign key actually lives

Why "what shape is this relationship" matters

Module 2 introduced foreign keys as the way one table points at another. What it didn't say is that the shape of the relationship between those two tables determines exactly where the foreign key column should go and whether you might need a third table that nobody mentioned yet.

Every relationship between two tables falls into one of three shapes. Once you can recognize which shape you're looking at, the foreign key's position stops being a guess. It becomes mechanical.

One-to-many — the most common shape

A one-to-many relationship means: one row in Table A is connected to many rows in Table B, but each row in Table B is only connected to one row in Table A.

A user has many posts. Each post belongs to exactly one user. That's one-to-many. A customer has many orders, each order belongs to one customer. That's one-to-many. A pet has many vet visits, each visit belongs to one pet. That's one-to-many.

Once you start naming these out loud, it becomes obvious that one-to-many is everywhere. It's the default shape relational databases were built around.

Where the foreign key goes

The foreign key lives on the "many" side. Always. Every row on the many side carries a pointer to its one row on the other side.

users and posts — one user, many posts

CREATE TABLE users (
  id    UUID PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL
);

CREATE TABLE posts (
  id        UUID PRIMARY KEY,
  user_id   UUID REFERENCES users(id),   -- the FK lives here, on the "many" side
  title     TEXT NOT NULL,
  body      TEXT
);

Every post row has a user_id pointing at exactly one user. The users table has no idea how many posts each user has — that's discoverable by querying the posts table, not by storing it on users.

Many-to-many — when both sides have many

A many-to-many relationship means: one row in Table A connects to many rows in Table B, and one row in Table B connects to many rows in Table A.

A book has many authors, an author writes many books. That's many-to-many. A student takes many classes, a class has many students. Many-to-many. A recipe has many ingredients, an ingredient appears in many recipes. Many-to-many.

Where the foreign key goes — the join table trick

Many-to-many can't be expressed with one foreign key column on either side. Both sides have "many," and a column can only hold one value per row. So relational databases solve it with a third table whose entire job is to record the connections.

books and authors — neither table holds the FK

CREATE TABLE books (
  id    UUID PRIMARY KEY,
  title TEXT NOT NULL
);

CREATE TABLE authors (
  id    UUID PRIMARY KEY,
  name  TEXT NOT NULL
);

-- The join table — its only job is to connect the other two.
CREATE TABLE book_authors (
  book_id   UUID REFERENCES books(id),
  author_id UUID REFERENCES authors(id),
  PRIMARY KEY (book_id, author_id)
);

The book_authors table is a join table (sometimes called a junction table or pivot table). Each row in it represents one connection: "this book has this author." A book with three authors creates three rows in book_authors. An author who wrote ten books creates ten rows.

The foreign keys live on the join table, not on either of the two main tables. Books doesn't know who its authors are directly. Authors doesn't know what books they wrote directly. Both discoveries happen by joining through the junction table.

One-to-one — the rarest shape

A one-to-one relationship means: one row in Table A connects to exactly one row in Table B, and vice versa. Neither side has many.

This is the rarest of the three because most "one-to-one" relationships could just be a single table with more columns. If a user has exactly one profile, why not put the profile fields directly on the user row?

When one-to-one is actually the right answer

A few honest reasons to split a one-to-one across two tables:

Where the foreign key goes

Either side can hold the FK. The convention is to put it on the table that's "secondary" or "optional" — the profile points at the user, not the other way around — and to add a UNIQUE constraint on the FK column to enforce that the relationship really is one-to-one.

users and profiles — one-to-one

CREATE TABLE users (
  id    UUID PRIMARY KEY,
  email TEXT UNIQUE NOT NULL
);

CREATE TABLE profiles (
  id        UUID PRIMARY KEY,
  user_id   UUID UNIQUE REFERENCES users(id),  -- UNIQUE is what makes it 1:1
  bio       TEXT,
  avatar_url TEXT
);

Without the UNIQUE on user_id, this would technically be a one-to-many: nothing would stop two profile rows from pointing at the same user. The UNIQUE constraint is what enforces the "one-to-one" promise at the database level.

The shape decides the layout

Every relational schema in every app you've ever used is some combination of these three shapes. There isn't a fourth. Once you can look at two nouns in a product and confidently say which shape connects them, you've cleared the biggest design hurdle in relational data.

The next page goes deeper on foreign keys themselves — what they actually are under the hood, and the surprising fact that they don't always have to point at a primary key.