EHR Systems Guide
Complete guide to Electronic Health Record (EHR) system architecture. Learn database design patterns, patient data models, clinical workflows, and interoperability standards for modern healthcare IT.
What is an EHR System?
An Electronic Health Record (EHR) is a digital version of a patient's complete medical history maintained by healthcare providers over time. Unlike EMRs (Electronic Medical Records) which are limited to a single organization, EHRs are designed to be shared across different healthcare settings and organizations.
Modern EHR systems integrate clinical, administrative, and financial data to support care coordination, population health management, and value-based care initiatives. They serve as the central data platform for healthcare delivery.
EMR vs EHR: Key Differences
EMR (Electronic Medical Record)
- •Digital version of paper chart
- •Used within ONE organization
- •Limited sharing capabilities
- •Focus on clinical documentation
- •Provider-centric view
EHR (Electronic Health Record)
- •Comprehensive health record
- •Shared across MULTIPLE organizations
- •Interoperability with HIE, labs, pharmacies
- •Supports care coordination & analytics
- •Patient-centric longitudinal view
Core EHR Database Schema
Production-grade PostgreSQL schema for EHR core modules:
-- ============================================ -- PATIENT DEMOGRAPHICS -- ============================================ CREATE TABLE patient ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identifiers mrn VARCHAR(50) UNIQUE NOT NULL, -- Medical Record Number ssn_encrypted BYTEA, -- Encrypted SSN -- Demographics first_name VARCHAR(100) NOT NULL, middle_name VARCHAR(100), last_name VARCHAR(100) NOT NULL, maiden_name VARCHAR(100), dob DATE NOT NULL, gender VARCHAR(20), sex_at_birth VARCHAR(20), -- Contact phone_primary VARCHAR(20), phone_secondary VARCHAR(20), email VARCHAR(255), -- Address address_line1 VARCHAR(255), address_line2 VARCHAR(255), city VARCHAR(100), state VARCHAR(2), zip_code VARCHAR(10), country VARCHAR(3) DEFAULT 'USA', -- Status status VARCHAR(20) DEFAULT 'active', -- active, inactive, deceased deceased_date DATE, -- Preferences preferred_language VARCHAR(50), race VARCHAR(100), ethnicity VARCHAR(100), -- Metadata created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_patient_mrn ON patient(mrn); CREATE INDEX idx_patient_name ON patient(last_name, first_name); CREATE INDEX idx_patient_dob ON patient(dob); -- ============================================ -- ENCOUNTERS (Visits) -- ============================================ CREATE TABLE encounter ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Patient patient_id UUID NOT NULL REFERENCES patient(id), -- Visit Details encounter_number VARCHAR(50) UNIQUE NOT NULL, encounter_type VARCHAR(100), -- inpatient, outpatient, emergency visit_type VARCHAR(100), -- office visit, telehealth, etc. -- Location facility_id UUID, department_id UUID, room_number VARCHAR(20), -- Provider attending_provider UUID, -- Reference to provider table referring_provider UUID, -- Timing admission_date TIMESTAMP NOT NULL, discharge_date TIMESTAMP, -- Status status VARCHAR(50), -- arrived, in-progress, completed, cancelled -- Classification chief_complaint TEXT, admit_reason TEXT, discharge_disposition VARCHAR(100), -- Financial insurance_id UUID, authorization_num VARCHAR(100), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_encounter_patient ON encounter(patient_id); CREATE INDEX idx_encounter_date ON encounter(admission_date); CREATE INDEX idx_encounter_status ON encounter(status); -- ============================================ -- CLINICAL NOTES -- ============================================ CREATE TABLE clinical_note ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), encounter_id UUID NOT NULL REFERENCES encounter(id), patient_id UUID NOT NULL REFERENCES patient(id), -- Note Details note_type VARCHAR(100) NOT NULL, -- progress, H&P, discharge, consult note_title VARCHAR(255), note_text TEXT NOT NULL, -- Provider author_id UUID NOT NULL, signed_by UUID, signed_at TIMESTAMP, -- Status status VARCHAR(50) DEFAULT 'draft', -- draft, signed, amended, deleted -- Metadata created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_note_encounter ON clinical_note(encounter_id); CREATE INDEX idx_note_patient ON clinical_note(patient_id); CREATE INDEX idx_note_type ON clinical_note(note_type); -- ============================================ -- PROBLEM LIST (Diagnoses) -- ============================================ CREATE TABLE problem ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), patient_id UUID NOT NULL REFERENCES patient(id), encounter_id UUID REFERENCES encounter(id), -- Problem Details icd10_code VARCHAR(10), snomed_code VARCHAR(20), problem_description TEXT NOT NULL, -- Classification problem_type VARCHAR(50), -- diagnosis, symptom, finding severity VARCHAR(20), -- mild, moderate, severe -- Status status VARCHAR(50) DEFAULT 'active', -- active, resolved, chronic onset_date DATE, resolved_date DATE, -- Clinical is_chronic BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_problem_patient ON problem(patient_id); CREATE INDEX idx_problem_status ON problem(status); CREATE INDEX idx_problem_icd10 ON problem(icd10_code); -- ============================================ -- MEDICATIONS -- ============================================ CREATE TABLE medication ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), patient_id UUID NOT NULL REFERENCES patient(id), encounter_id UUID REFERENCES encounter(id), -- Drug Information rxcui VARCHAR(20), -- RxNorm code ndc VARCHAR(20), drug_name VARCHAR(500) NOT NULL, -- Dosage strength VARCHAR(100), dose_form VARCHAR(100), -- tablet, capsule, injection route VARCHAR(50), -- oral, IV, topical frequency VARCHAR(100), -- BID, TID, QID, PRN -- Instructions sig TEXT, -- Prescription instructions quantity DECIMAL(10,2), refills INTEGER, -- Dates start_date DATE NOT NULL, end_date DATE, -- Provider prescriber_id UUID, prescriber_npi VARCHAR(10), -- Status status VARCHAR(50) DEFAULT 'active', -- active, discontinued, completed created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_medication_patient ON medication(patient_id); CREATE INDEX idx_medication_status ON medication(status); CREATE INDEX idx_medication_rxcui ON medication(rxcui); -- ============================================ -- VITAL SIGNS -- ============================================ CREATE TABLE vital_signs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), patient_id UUID NOT NULL REFERENCES patient(id), encounter_id UUID REFERENCES encounter(id), -- Vital Measurements temperature DECIMAL(5,2), -- Fahrenheit temperature_unit VARCHAR(1) DEFAULT 'F', -- F or C bp_systolic INTEGER, -- mmHg bp_diastolic INTEGER, -- mmHg heart_rate INTEGER, -- bpm respiratory_rate INTEGER, -- per minute oxygen_saturation DECIMAL(5,2), -- SpO2 % oxygen_flow_rate DECIMAL(5,2), -- L/min height DECIMAL(6,2), -- inches weight DECIMAL(6,2), -- pounds bmi DECIMAL(5,2), -- calculated -- Context measured_at TIMESTAMP NOT NULL DEFAULT NOW(), measured_by UUID, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_vitals_patient ON vital_signs(patient_id); CREATE INDEX idx_vitals_encounter ON vital_signs(encounter_id); CREATE INDEX idx_vitals_measured ON vital_signs(measured_at); -- ============================================ -- LAB RESULTS -- ============================================ CREATE TABLE lab_result ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), patient_id UUID NOT NULL REFERENCES patient(id), encounter_id UUID REFERENCES encounter(id), order_id UUID, -- Test Details loinc_code VARCHAR(10), test_name VARCHAR(255) NOT NULL, -- Result result_value VARCHAR(500), result_numeric DECIMAL(18,6), result_unit VARCHAR(50), -- Reference Range reference_low DECIMAL(18,6), reference_high DECIMAL(18,6), reference_text VARCHAR(255), -- Interpretation abnormal_flag VARCHAR(20), -- H, L, N, HH, LL status VARCHAR(50), -- final, preliminary, corrected -- Timing collected_at TIMESTAMP, resulted_at TIMESTAMP, -- Provider ordering_provider UUID, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_lab_patient ON lab_result(patient_id); CREATE INDEX idx_lab_loinc ON lab_result(loinc_code); CREATE INDEX idx_lab_resulted ON lab_result(resulted_at);
EHR Core Modules
Patient Management
Demographics, registration, insurance, MPI (Master Patient Index), patient portal
Scheduling & Registration
Appointments, provider calendars, patient check-in, encounter creation
Clinical Documentation
Progress notes, H&P, discharge summaries, templates, clinical decision support
Clinical Orders & Results
Lab orders, imaging, medications, procedures, result management
Revenue Cycle Management
Charge capture, claims generation, billing, denial management, collections
Common EHR Queries
Get Patient Summary
SELECT p.mrn, p.first_name || ' ' || p.last_name as patient_name, p.dob, p.gender, COUNT(DISTINCT e.id) as total_visits, COUNT(DISTINCT pr.id) as active_problems, COUNT(DISTINCT m.id) as active_medications FROM patient p LEFT JOIN encounter e ON p.id = e.patient_id LEFT JOIN problem pr ON p.id = pr.patient_id AND pr.status = 'active' LEFT JOIN medication m ON p.id = m.patient_id AND m.status = 'active' WHERE p.mrn = 'MRN12345' GROUP BY p.id, p.mrn, p.first_name, p.last_name, p.dob, p.gender;
Find Patients with Diabetes Not on Metformin
SELECT DISTINCT
p.mrn,
p.first_name || ' ' || p.last_name as patient_name,
pr.problem_description,
pr.onset_date
FROM patient p
JOIN problem pr ON p.id = pr.patient_id
WHERE pr.icd10_code LIKE 'E11%' -- Type 2 Diabetes
AND pr.status = 'active'
AND NOT EXISTS (
SELECT 1 FROM medication m
WHERE m.patient_id = p.id
AND m.drug_name ILIKE '%metformin%'
AND m.status = 'active'
);