Normalization removes the ways data can contradict itself. Constraints remove the ways it can be wrong in the first place. Application code can enforce neither โ it is not the only thing writing to your database.
3a
The clinic sends you their existing booking sheet. Walk it with Next โ each normal form removes one specific way the data can lie.
speciality_1 / speciality_2 columns. Repeating groups become rows in another table.doctor_name depends on doctor_id, not on the appointment โ so it moves.The three anomalies have names. Update โ a doctor renames and you miss a row, so the database now holds two versions of one truth. Insert โ a new speciality cannot be recorded until some doctor has it. Delete โ cancelling the last appointment erases the only record of the doctor's phone number.
3b
A constraint is a rule the database refuses to break, for every writer โ your API, a migration script, a colleague at a psql prompt, a second service you have not built yet.
CREATE TABLE appointments ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, patient_id integer NOT NULL REFERENCES patients(id) ON DELETE RESTRICT, doctor_id integer NOT NULL REFERENCES doctors(id) ON DELETE RESTRICT, starts_at timestamptz NOT NULL, duration_min integer NOT NULL DEFAULT 15, status text NOT NULL DEFAULT 'booked', fee numeric(10,2) NOT NULL, CONSTRAINT fee_not_negative CHECK (fee >= 0), CONSTRAINT duration_sane CHECK (duration_min BETWEEN 5 AND 240), CONSTRAINT status_known CHECK (status IN ('booked','attended','cancelled','no_show')), CONSTRAINT one_slot_per_doctor UNIQUE (doctor_id, starts_at) -- the double-booking rule );
NOT NULL is a decision, not a default. For each column ask: is a row meaningful without this? An appointment without a starts_at is not an appointment.UNIQUE encodes a real-world rule. (doctor_id, starts_at) says a doctor exists once per instant โ which is physics, not policy.CHECK is the cheapest validation you will ever write and the only one that cannot be bypassed. Name your constraints โ fee_not_negative is what your error handler will read in Session 09.ON DELETE is part of the model. RESTRICT refuses to delete a patient who has appointments; CASCADE would silently erase their history. For clinical records, refusing is correct.Adding NOT NULL or a CHECK to a table that already holds data. PostgreSQL validates every existing row first and rejects the whole statement if one fails. Fix the data, then add the rule โ and expect that order to matter again in Session 06.
3c
This is the one requirement from the brief that application code cannot keep. It is worth seeing exactly why.
-- Request A -- Request B (2ms later) SELECT count(*) FROM appointments SELECT count(*) FROM appointments WHERE doctor_id = 7 WHERE doctor_id = 7 AND starts_at = '09:30'; AND starts_at = '09:30'; -- returns 0, so "the slot is free" -- also returns 0. Also "free". INSERT INTO appointments ... INSERT INTO appointments ... -- succeeds -- ALSO SUCCEEDS. Slot sold twice.
SELECT and the INSERT. Adding a mutex fixes one process and breaks the moment you run two.UNIQUE constraint is checked at write time, atomically. The second INSERT fails, the database raises a unique-violation, and no slot is ever sold twice โ regardless of how many servers you run.409 Conflict with a useful message.UNIQUE is not enough. PostgreSQL's EXCLUDE USING gist with a tstzrange handles it; MySQL has no equivalent and needs a locking read.Take this sheet: booking_id, patient_name, patient_phone, doctor_name, doctor_licence, room, procedures, fee โ where procedures holds "x-ray, blood test". (a) Normalize it to 3NF and list your final tables with their keys. (b) Name the update anomaly that exists before you start and say which normal form removes it. (c) Write three named CHECK constraints for the result, and one UNIQUE that encodes a rule the clinic would actually care about.
Recap
NOT NULL, UNIQUE, CHECK and foreign-key actions are rules every writer obeys โ name them, because your error handler will read those names.UNIQUE (doctor_id, starts_at) settles it atomically.