Session 02 ยท ๐Ÿ”— Relationships

One-to-One, One-to-Many & Many-to-Many

Three shapes, and only three. Getting a relationship right is almost always a question of which table holds the foreign key โ€” and for one of the three shapes, the honest answer is "neither".

โœ“
2aone-to-many ยท the default
โœ“
2bone-to-one ยท and its price
โœ“
2cmany-to-many ยท the join table

2a

Three sentences, three different shapes

The brief already told you the cardinality. Walk it with Next โ€” each sentence becomes a relationship, and two of them have a tempting wrong version first.

1:NThe foreign key goes on the "many" side. A patient has many appointments, so patient_id lives on appointments โ€” one value per row, which is the only shape a column can hold.
1:1A one-to-one is a one-to-many with a UNIQUE on it. Put the key on the optional side, then make it unique โ€” that is the entire difference.
N:MMany-to-many cannot be expressed with a column at all. Neither side can hold a list, so the relationship becomes its own table.

Read the sentence in both directions. "A doctor has specialities" sounds one-to-many until you ask the other way: does a speciality have many doctors? It does โ€” and that second reading is what forces the join table.

2b

One-to-one, and whether you want it at all

A patient has exactly one medical record. That is real โ€” but it is also the relationship people reach for when they should have added columns.

one-to-one โ€” the key goes on the dependent side
CREATE TABLE medical_records (
  id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  patient_id  integer NOT NULL UNIQUE REFERENCES patients(id) ON DELETE CASCADE,
  blood_type  text,
  allergies   text[],
  notes       text
);
-- UNIQUE is what turns one-to-many into one-to-one.
-- Drop it and a patient silently acquires a second record.
+Split when the halves have different lifetimes or access rules. Medical notes are sensitive, read rarely, and may need separate permissions โ€” that is a real reason to keep them in their own table.
+Split when one half is mostly empty. If nine patients in ten have no record yet, a separate table stores nothing for them instead of ten null columns.
โœ—Do not split just to tidy up. Moving four columns into a patient_details table because the main one "looks long" buys you a mandatory join on every read and no guarantee the row exists.
โ†’ON DELETE CASCADE is defensible here precisely because the record cannot exist without its patient. That is rarely true for the other two shapes.
Mistake

Putting the foreign key on the parent โ€” patients.medical_record_id. It works, but now a patient row cannot be created until its record exists, the column is null for most rows, and nothing stops two patients pointing at the same record. Key on the dependent side, always.

2c

Many-to-many: the relationship becomes a table

A doctor has many specialities; a speciality has many doctors. No column on either table can hold that, so the link gets a table of its own.

many-to-many โ€” the join table
CREATE TABLE specialities (
  id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  text NOT NULL UNIQUE
);

CREATE TABLE doctor_specialities (
  doctor_id      integer NOT NULL REFERENCES doctors(id)       ON DELETE CASCADE,
  speciality_id  integer NOT NULL REFERENCES specialities(id) ON DELETE RESTRICT,
  certified_on   date,                      -- the join table can carry its own data
  PRIMARY KEY (doctor_id, speciality_id)   -- composite: one pairing, once
);
PKThe composite primary key is the point. PRIMARY KEY (doctor_id, speciality_id) makes the same pairing impossible twice โ€” without it, "cardiology" can be added to the same doctor forty times.
โ†’Join tables grow attributes. certified_on belongs to the pairing, not to the doctor and not to the speciality. The moment you notice a field like that, the join table has become an entity in its own right.
โ†’The two ON DELETE actions differ on purpose. Deleting a doctor should remove their pairings (CASCADE); deleting a speciality that doctors still hold should fail (RESTRICT).
โœ—The comma-separated column. specialities text holding 'cardiology,neurology' cannot be joined, indexed, constrained or counted. It is the single most common modelling mistake, and Session 03 shows exactly why it fails.
TASK

Add rooms to the schema โ€” and pick the right shape

The clinic adds rooms. (a) An appointment happens in exactly one room, and a room hosts many appointments โ€” write that relationship and say which table gets the foreign key. (b) A room can be equipped for several procedures, and a procedure can happen in several rooms โ€” write that one too, including the composite primary key. (c) One of your two join columns wants an ON DELETE CASCADE and one wants RESTRICT. Decide which, and defend it in a sentence.

Recap

Session 02 in four lines

1One-to-many is the default shape, and the foreign key always sits on the "many" side.
2One-to-one is one-to-many plus UNIQUE, keyed on the dependent side โ€” and is worth it only for different lifetimes, permissions or sparsity.
3Many-to-many needs its own table with a composite primary key; neither side can hold a list.
4Read every sentence in both directions โ€” the reverse reading is what reveals a hidden many-to-many.