A schema is a reading of the requirements. Before any Go exists, you decide what things are, how a row is identified, and what each column is allowed to hold โ and every one of those decisions is expensive to reverse.
1a
The client gives you prose. You need entities. Walk it with Next โ six steps from one sentence to three tables.
starts_at, not a table.Getting this wrong is the expensive mistake. A wrong column type is one migration. A missing entity is a rewrite of every query, struct and handler that touched it โ which is why sessions 01 to 04 happen before a single line of Go.
1b
Two jobs, often confused. A primary key answers "which row is this?" โ a foreign key answers "which row over there does it belong to?"
CREATE TABLE patients ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, full_name text NOT NULL, date_of_birth date NOT NULL, phone text, -- text, not integer โ see 1c created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE doctors ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, full_name text NOT NULL, licence_no text NOT NULL UNIQUE, -- a natural key, kept as a constraint created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE appointments ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, patient_id integer NOT NULL REFERENCES patients(id), doctor_id integer NOT NULL REFERENCES doctors(id), starts_at timestamptz NOT NULL, duration_min integer NOT NULL DEFAULT 15, status text NOT NULL DEFAULT 'booked', fee numeric(10,2) NOT NULL );
licence_no identifies a doctor in the real world, but real-world identifiers get reissued, corrected and mistyped. Use a meaningless id as the key and keep the natural one as a UNIQUE constraint โ you get both.patient_id sits on appointments. Session 02 makes that rule general.GENERATED ALWAYS AS IDENTITY is the modern standard spelling. serial still works, but identity columns are SQL-standard and cannot be overwritten by accident.REFERENCES is just a number. Without the constraint the database will happily store patient_id = 9999 for a patient that never existed.MySQL spells the identity column id INT AUTO_INCREMENT PRIMARY KEY and needs ENGINE=InnoDB for foreign keys to be enforced at all โ MyISAM parses REFERENCES and silently ignores it.
1c
Every column has a tempting type and a correct one. The tempting one is almost always text or float.
'2026-03-01 09:30' < '2026-03-01 10:00' happens to work; add a timezone and it stops. timestamptz stores an instant and does the arithmetic for you.0.1 + 0.2 is not 0.3 in binary floating point, and after ten thousand invoices the total is visibly wrong. numeric is exact decimal.+, and invites someone to do arithmetic on it. A phone number is an identifier that happens to use digits.date vs timestamptz. A birthday has no time and no timezone โ storing it as an instant means someone born on the 1st appears to be born on the 31st in another region.Reaching for varchar(50) out of habit. In PostgreSQL varchar(n) and text are the same type with a length check bolted on โ the limit buys no performance, and it is the constraint you will most often have to migrate away. Use text, and add a CHECK when a real limit exists.
Take this brief: "A clinic runs rooms. Each appointment happens in one room. Rooms have a floor and a capacity, and some are equipped for X-ray." (a) Decide whether room is an entity or a column on appointments, and write one sentence defending it. (b) Write the CREATE TABLE for whatever you decided, with a primary key and the right type for every column. (c) Name the one column where you were tempted by text and chose something better.
Recap
id, and keep real-world identifiers as UNIQUE.REFERENCES.timestamptz not text, numeric not float, text not integer for phone numbers.