Hospital Management ER Diagram

A hospital schema connects people, resources and billing: departments group doctors, appointments pair a patient with a doctor, and prescriptions issued during an appointment carry their medication lines. Admissions record inpatient stays, and each admission produces one invoice.

The schema keeps clinical encounters and inpatient stays apart. An appointment is a scheduled outpatient contact, while admissions records a patient occupying a bed over a date range and carries its own daily_rate. A prescription belongs to an appointment through appointment_id, which is UNIQUE here: one encounter, one prescription sheet, with prescription_items holding the individual medication lines. That separation lets a single visit prescribe several drugs without duplicating the appointment, and keeps the header notes and issued_at timestamp in one place. It also means a prescription can be amended without touching the appointment schedule or status.

Read the diagram left to right: departments own doctors, patients connect to doctors through appointments, and prescriptions continue the encounter chain down to medications. prescription_items also references medications directly, so a medication appears in many prescriptions over time, and its quantity column carries a CHECK that rejects zero or negative values. On the inpatient side, admissions points at both patient_id and doctor_id, and invoices.admission_id is UNIQUE, turning the usual one-to-many into one invoice per stay. The room_number and bed_number columns keep the physical bed assignment on the admission. Each edge therefore answers a different operational question.

Two operational rules matter here. Deleting a patient with appointments should not erase clinical history, so appointments.patient_id has no cascade; deleting a doctor is equally blocked while appointments or admissions still reference them. Prescriptions and their items, by contrast, cascade from the appointment because they are detail records. The admitted_at and discharged_at pair also needs a CHECK that discharged_at is null or later than admission, otherwise a typo can produce a negative length of stay — and invoice totals that nobody can explain. None of these rules can be enforced from the application layer alone.

The most useful extension is a normalized rooms table (room_number, room_type, floor, capacity), referenced by admissions.room_id; it makes occupancy reports, ward assignments and daily cleaning schedules possible, which a free-text room number cannot support, and it lets housekeeping mark a bed out of service without editing an admission. A separate vitals or lab_results table keyed by appointment gives clinicians measurements without adding columns to the encounter. Finally, splitting invoices into invoice_items (description, amount) lets one bill cover bed days, procedures and medication charges, instead of storing a single opaque amount.

The diagram

1:N 1:N 1:N 1:1 1:N 1:N 1:N 1:N 1:1 departments PKid integer Uname varchar(100) floor integer doctors PKid integer FKdepartment_id integer full_name varchar(120) speciality varchar(80) Uemail varchar(255) patients PKid integer full_name varchar(120) date_of_birth date Uemail varchar(255) phone varchar(30) appointments PKid integer FKpatient_id integer FKdoctor_id integer scheduled_for timestamp status varchar(20) notes text medications PKid integer Uname varchar(120) strength varchar(40) prescriptions PKid integer FKappointment_id integer issued_at timestamp notes text prescription_items PKid integer FKprescription_id integer FKmedication_id integer dosage varchar(80) quantity integer admissions PKid integer FKpatient_id integer FKdoctor_id integer room_number varchar(10) bed_number varchar(10) admitted_at timestamp discharged_at timestamp daily_rate decimal(10,2) invoices PKid integer FKadmission_id integer amount decimal(12,2) status varchar(20) issued_at timestamp

Tables (9)

departments

  • idintegerPK
  • namevarchar(100)U
  • floorinteger

doctors

  • idintegerPK
  • department_idintegerFK
  • full_namevarchar(120)
  • specialityvarchar(80)
  • emailvarchar(255)U

patients

  • idintegerPK
  • full_namevarchar(120)
  • date_of_birthdate
  • emailvarchar(255)U
  • phonevarchar(30)

appointments

  • idintegerPK
  • patient_idintegerFK
  • doctor_idintegerFK
  • scheduled_fortimestamp
  • statusvarchar(20)
  • notestext

medications

  • idintegerPK
  • namevarchar(120)U
  • strengthvarchar(40)

prescriptions

  • idintegerPK
  • appointment_idintegerFKU
  • issued_attimestamp
  • notestext

prescription_items

  • idintegerPK
  • prescription_idintegerFK
  • medication_idintegerFK
  • dosagevarchar(80)
  • quantityinteger

admissions

  • idintegerPK
  • patient_idintegerFK
  • doctor_idintegerFK
  • room_numbervarchar(10)
  • bed_numbervarchar(10)
  • admitted_attimestamp
  • discharged_attimestamp
  • daily_ratedecimal(10,2)

invoices

  • idintegerPK
  • admission_idintegerFKU
  • amountdecimal(12,2)
  • statusvarchar(20)
  • issued_attimestamp

Relationships (9)

  • 1:Ndepartments.id→doctors.department_id
  • 1:Npatients.id→appointments.patient_id
  • 1:Ndoctors.id→appointments.doctor_id
  • 1:1appointments.id→prescriptions.appointment_idON DELETE CASCADE
  • 1:Nprescriptions.id→prescription_items.prescription_idON DELETE CASCADE
  • 1:Nmedications.id→prescription_items.medication_id
  • 1:Npatients.id→admissions.patient_id
  • 1:Ndoctors.id→admissions.doctor_id
  • 1:1admissions.id→invoices.admission_idON DELETE CASCADE

The SQL

Copy this, paste it in the SQL tab, or just open it below.
SQL
CREATE TABLE departments (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL UNIQUE,
  floor INTEGER
);

CREATE TABLE doctors (
  id SERIAL PRIMARY KEY,
  department_id INTEGER REFERENCES departments(id),
  full_name VARCHAR(120) NOT NULL,
  speciality VARCHAR(80) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE patients (
  id SERIAL PRIMARY KEY,
  full_name VARCHAR(120) NOT NULL,
  date_of_birth DATE NOT NULL,
  email VARCHAR(255) UNIQUE,
  phone VARCHAR(30)
);

CREATE TABLE appointments (
  id SERIAL PRIMARY KEY,
  patient_id INTEGER NOT NULL REFERENCES patients(id),
  doctor_id INTEGER NOT NULL REFERENCES doctors(id),
  scheduled_for TIMESTAMP NOT NULL,
  status VARCHAR(20) NOT NULL DEFAULT 'scheduled',
  notes TEXT
);

CREATE TABLE medications (
  id SERIAL PRIMARY KEY,
  name VARCHAR(120) NOT NULL UNIQUE,
  strength VARCHAR(40)
);

CREATE TABLE prescriptions (
  id SERIAL PRIMARY KEY,
  appointment_id INTEGER NOT NULL UNIQUE REFERENCES appointments(id) ON DELETE CASCADE,
  issued_at TIMESTAMP NOT NULL DEFAULT now(),
  notes TEXT
);

CREATE TABLE prescription_items (
  id SERIAL PRIMARY KEY,
  prescription_id INTEGER NOT NULL REFERENCES prescriptions(id) ON DELETE CASCADE,
  medication_id INTEGER NOT NULL REFERENCES medications(id),
  dosage VARCHAR(80) NOT NULL,
  quantity INTEGER NOT NULL CHECK (quantity > 0),
  UNIQUE (prescription_id, medication_id)
);

CREATE TABLE admissions (
  id SERIAL PRIMARY KEY,
  patient_id INTEGER NOT NULL REFERENCES patients(id),
  doctor_id INTEGER NOT NULL REFERENCES doctors(id),
  room_number VARCHAR(10) NOT NULL,
  bed_number VARCHAR(10),
  admitted_at TIMESTAMP NOT NULL DEFAULT now(),
  discharged_at TIMESTAMP,
  daily_rate DECIMAL(10,2) NOT NULL CHECK (daily_rate > 0),
  CHECK (discharged_at IS NULL OR discharged_at >= admitted_at)
);

CREATE TABLE invoices (
  id SERIAL PRIMARY KEY,
  admission_id INTEGER NOT NULL UNIQUE REFERENCES admissions(id) ON DELETE CASCADE,
  amount DECIMAL(12,2) NOT NULL CHECK (amount >= 0),
  status VARCHAR(20) NOT NULL DEFAULT 'open',
  issued_at TIMESTAMP NOT NULL DEFAULT now()
);

Keep learning