SQLExplain Team
How to Design a Database Schema for a Healthcare Appointment System in PostgreSQL
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
Key Tables
patients
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
first_name | VARCHAR(100) | |
last_name | VARCHAR(100) | |
email | VARCHAR(255) | Unique |
phone | VARCHAR(20) | |
date_of_birth | DATE | |
gender | VARCHAR(20) | |
address | TEXT | |
emergency_contact_name | VARCHAR(200) | |
emergency_contact_phone | VARCHAR(20) | |
created_at | TIMESTAMP |
providers
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
first_name | VARCHAR(100) | |
last_name | VARCHAR(100) | |
email | VARCHAR(255) | Unique |
phone | VARCHAR(20) | |
specialization | VARCHAR(100) | |
license_number | VARCHAR(50) | Unique |
created_at | TIMESTAMP |
clinics
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
name | VARCHAR(200) | |
address | TEXT | |
phone | VARCHAR(20) | |
operating_hours | JSONB | Flexible schedule storage |
created_at | TIMESTAMP |
provider_clinics
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
provider_id | UUID | FK → providers.id |
clinic_id | UUID | FK → clinics.id |
is_primary | BOOLEAN |
availability_slots
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
provider_id | UUID | FK → providers.id |
clinic_id | UUID | FK → clinics.id |
day_of_week | INTEGER | 0–6 (Sunday–Saturday) |
start_time | TIME | |
end_time | TIME | |
slot_duration_minutes | INTEGER | |
is_active | BOOLEAN |
appointments
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
patient_id | UUID | FK → patients.id |
provider_id | UUID | FK → providers.id |
clinic_id | UUID | FK → clinics.id |
scheduled_at | TIMESTAMP | |
duration_minutes | INTEGER | |
status | VARCHAR(20) | scheduled, confirmed, completed, cancelled, no_show |
reason | TEXT | |
notes | TEXT | |
created_at | TIMESTAMP | |
updated_at | TIMESTAMP |
medical_records
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
appointment_id | UUID | FK → appointments.id |
patient_id | UUID | FK → patients.id |
provider_id | UUID | FK → providers.id |
diagnosis | TEXT | |
symptoms | TEXT | |
treatment_notes | TEXT | |
vitals | JSONB | Blood pressure, heart rate, temperature, etc. |
created_at | TIMESTAMP |
prescriptions
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
medical_record_id | UUID | FK → medical_records.id |
patient_id | UUID | FK → patients.id |
provider_id | UUID | FK → providers.id |
medication_name | VARCHAR(200) | |
dosage | VARCHAR(100) | |
frequency | VARCHAR(100) | |
duration_days | INTEGER | |
instructions | TEXT | |
prescribed_at | TIMESTAMP |
insurance_plans
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
patient_id | UUID | FK → patients.id |
provider_name | VARCHAR(200) | |
policy_number | VARCHAR(100) | |
group_number | VARCHAR(100) | |
coverage_type | VARCHAR(50) | |
valid_from | DATE | |
valid_until | DATE |
invoices
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
appointment_id | UUID | FK → appointments.id |
patient_id | UUID | FK → patients.id |
insurance_plan_id | UUID | FK → insurance_plans.id (nullable) |
total_amount | DECIMAL(10,2) | |
insurance_covered | DECIMAL(10,2) | |
patient_due | DECIMAL(10,2) | |
status | VARCHAR(20) | pending, paid, overdue, refunded |
issued_at | TIMESTAMP | |
paid_at | TIMESTAMP | Nullable |
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_urlandappointment_type(in-person vs virtual) to theappointmentstable - Lab Results — create a
lab_teststable linked to medical records for blood work, imaging, etc. - Referrals — track provider-to-provider referrals with a
referralstable - 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.
