Back to Blog
S

SQLExplain Team

How to Design a Database Schema for a Healthcare Appointment System in PostgreSQL

Doctor reviewing patient records on a tablet in a modern clinic

How to Design a Database Schema for a Healthcare Appointment System

Introduction

A healthcare appointment system connects patients with healthcare providers, enabling scheduling, medical record management, and billing. Designing the database schema requires careful consideration of patient privacy, appointment logistics, provider availability, and medical history tracking.

In this post, we will walk through the key entities, relationships, and tables needed to build a robust healthcare appointment system.


Entities and Relationships

The core entities in a healthcare appointment system include:

  • Patients — individuals seeking medical care
  • Providers — doctors, specialists, nurses, and other healthcare professionals
  • Clinics/Facilities — physical locations where appointments take place
  • Appointments — scheduled meetings between patients and providers
  • Medical Records — patient health history and visit notes
  • Prescriptions — medications prescribed during or after visits
  • Insurance Plans — patient insurance coverage details
  • Billing/Invoices — charges for services rendered

Key Relationships

  • A patient can book many appointments with different providers
  • A provider works at one or more clinics and has defined availability slots
  • Each appointment generates medical records and may result in prescriptions
  • Billing is tied to appointments and may reference insurance plans

ER Diagram

Scroll or pinch to zoom · drag to pan · double-click to zoom in
Entity-relationship diagram for the healthcare appointment system schema
Entity-relationship diagram — healthcare appointment system

Key Tables

patients

ColumnTypeNotes
idUUIDPrimary key
first_nameVARCHAR(100)
last_nameVARCHAR(100)
emailVARCHAR(255)Unique
phoneVARCHAR(20)
date_of_birthDATE
genderVARCHAR(20)
addressTEXT
emergency_contact_nameVARCHAR(200)
emergency_contact_phoneVARCHAR(20)
created_atTIMESTAMP

providers

ColumnTypeNotes
idUUIDPrimary key
first_nameVARCHAR(100)
last_nameVARCHAR(100)
emailVARCHAR(255)Unique
phoneVARCHAR(20)
specializationVARCHAR(100)
license_numberVARCHAR(50)Unique
created_atTIMESTAMP

clinics

ColumnTypeNotes
idUUIDPrimary key
nameVARCHAR(200)
addressTEXT
phoneVARCHAR(20)
operating_hoursJSONBFlexible schedule storage
created_atTIMESTAMP

provider_clinics

ColumnTypeNotes
idUUIDPrimary key
provider_idUUIDFK → providers.id
clinic_idUUIDFK → clinics.id
is_primaryBOOLEAN

availability_slots

ColumnTypeNotes
idUUIDPrimary key
provider_idUUIDFK → providers.id
clinic_idUUIDFK → clinics.id
day_of_weekINTEGER0–6 (Sunday–Saturday)
start_timeTIME
end_timeTIME
slot_duration_minutesINTEGER
is_activeBOOLEAN

appointments

ColumnTypeNotes
idUUIDPrimary key
patient_idUUIDFK → patients.id
provider_idUUIDFK → providers.id
clinic_idUUIDFK → clinics.id
scheduled_atTIMESTAMP
duration_minutesINTEGER
statusVARCHAR(20)scheduled, confirmed, completed, cancelled, no_show
reasonTEXT
notesTEXT
created_atTIMESTAMP
updated_atTIMESTAMP

medical_records

ColumnTypeNotes
idUUIDPrimary key
appointment_idUUIDFK → appointments.id
patient_idUUIDFK → patients.id
provider_idUUIDFK → providers.id
diagnosisTEXT
symptomsTEXT
treatment_notesTEXT
vitalsJSONBBlood pressure, heart rate, temperature, etc.
created_atTIMESTAMP

prescriptions

ColumnTypeNotes
idUUIDPrimary key
medical_record_idUUIDFK → medical_records.id
patient_idUUIDFK → patients.id
provider_idUUIDFK → providers.id
medication_nameVARCHAR(200)
dosageVARCHAR(100)
frequencyVARCHAR(100)
duration_daysINTEGER
instructionsTEXT
prescribed_atTIMESTAMP

insurance_plans

ColumnTypeNotes
idUUIDPrimary key
patient_idUUIDFK → patients.id
provider_nameVARCHAR(200)
policy_numberVARCHAR(100)
group_numberVARCHAR(100)
coverage_typeVARCHAR(50)
valid_fromDATE
valid_untilDATE

invoices

ColumnTypeNotes
idUUIDPrimary key
appointment_idUUIDFK → appointments.id
patient_idUUIDFK → patients.id
insurance_plan_idUUIDFK → insurance_plans.id (nullable)
total_amountDECIMAL(10,2)
insurance_coveredDECIMAL(10,2)
patient_dueDECIMAL(10,2)
statusVARCHAR(20)pending, paid, overdue, refunded
issued_atTIMESTAMP
paid_atTIMESTAMPNullable

Example Queries

Find all upcoming appointments for a patient:

SELECT
  a.scheduled_at,
  pr.first_name AS doctor_first,
  pr.last_name  AS doctor_last,
  pr.specialization,
  c.name        AS clinic
FROM appointments a
JOIN providers pr ON a.provider_id = pr.id
JOIN clinics c    ON a.clinic_id   = c.id
JOIN patients p   ON a.patient_id  = p.id
WHERE a.patient_id = :patient_id
  AND a.scheduled_at > NOW()
  AND a.status IN ('scheduled', 'confirmed')
ORDER BY a.scheduled_at;

Get a provider's available slots for a given day:

SELECT
  s.start_time,
  s.end_time,
  s.slot_duration_minutes,
  c.name AS clinic
FROM availability_slots s
JOIN clinics c ON s.clinic_id = c.id
WHERE s.provider_id = :provider_id
  AND s.day_of_week = EXTRACT(DOW FROM :target_date::date)
  AND s.is_active = true;

Get full medical history for a patient:

SELECT
  mr.diagnosis,
  mr.symptoms,
  mr.treatment_notes,
  mr.vitals,
  a.scheduled_at,
  pr.first_name || ' ' || pr.last_name AS provider
FROM medical_records mr
JOIN appointments a ON mr.appointment_id = a.id
JOIN providers pr   ON mr.provider_id    = pr.id
WHERE mr.patient_id = :patient_id
ORDER BY a.scheduled_at DESC;

Calculate outstanding balance for a patient:

SELECT SUM(patient_due) AS total_outstanding
FROM invoices
WHERE patient_id = :patient_id
  AND status IN ('pending', 'overdue');

Extensibility

This schema can be extended in several ways:

  • Telemedicine — add a meeting_url and appointment_type (in-person vs virtual) to the appointments table
  • Lab Results — create a lab_tests table linked to medical records for blood work, imaging, etc.
  • Referrals — track provider-to-provider referrals with a referrals table
  • Reviews/Ratings — let patients rate providers after appointments
  • Notifications — store appointment reminders and notification preferences
  • Waitlist — manage cancellation slots with a waitlist queue per provider
  • Multi-language Support — add locale preferences to patients for communication

Try this schema in a live sandbox

Create a free SQLExplain account and land directly in a sandbox pre-loaded with the Healthcare Appointment Schema from this post — no password, no setup.