Readmission Risk Scoring — Multi-Factor Patient Stratification
Readmission Risk Scoring — Multi-Factor Patient Stratification
Predicting which patients are most likely to be readmitted within 30 days is one of the most impactful uses of hospital SQL analytics. This lesson builds a composite risk score from five clinical signals.
Risk Score Components
| Component | Signal | Weight |
|---|---|---|
| Age | Older patients → higher risk | +1 pt per decade > 50 |
| Prior admissions | More visits → higher risk | +2 pts per extra admission |
| Diagnosis count | More diagnoses → higher risk | +1 pt per diagnosis |
| Medication count | Polypharmacy → higher risk | +1 pt per medication |
| Abnormal labs | More abnormal results → higher risk | +2 pts per abnormal result |
1. Build the Risk Score CTE
WITH patient_age AS (
SELECT
id AS patient_id,
GREATEST(0, (DATE_PART('year', AGE(dob)) - 50) / 10)::INT AS age_score
FROM hc_patients
),
prior_admissions AS (
SELECT
patient_id,
GREATEST(0, COUNT(*) - 1) * 2 AS admission_score
FROM hc_admissions
WHERE admitted_at >= CURRENT_DATE - INTERVAL '24 months'
GROUP BY patient_id
),
diagnosis_score AS (
SELECT
a.patient_id,
COUNT(d.id) AS diag_score
FROM hc_diagnoses d
JOIN hc_admissions a ON a.id = d.admission_id
GROUP BY a.patient_id
),
medication_score AS (
SELECT
a.patient_id,
COUNT(m.id) AS med_score
FROM hc_medications m
JOIN hc_admissions a ON a.id = m.admission_id
GROUP BY a.patient_id
),
lab_score AS (
SELECT
patient_id,
COUNT(*) FILTER (WHERE is_abnormal) * 2 AS lab_abnormal_score
FROM hc_lab_results
GROUP BY patient_id
),
composite AS (
SELECT
p.id AS patient_id,
p.name,
p.mrn,
p.insurance_type,
COALESCE(pa.age_score, 0) AS age_score,
COALESCE(pr.admission_score, 0) AS admission_score,
COALESCE(ds.diag_score, 0) AS diag_score,
COALESCE(ms.med_score, 0) AS med_score,
COALESCE(ls.lab_abnormal_score, 0) AS lab_score,
COALESCE(pa.age_score, 0)
+ COALESCE(pr.admission_score, 0)
+ COALESCE(ds.diag_score, 0)
+ COALESCE(ms.med_score, 0)
+ COALESCE(ls.lab_abnormal_score, 0) AS total_risk_score
FROM hc_patients p
LEFT JOIN patient_age pa ON pa.patient_id = p.id
LEFT JOIN prior_admissions pr ON pr.patient_id = p.id
LEFT JOIN diagnosis_score ds ON ds.patient_id = p.id
LEFT JOIN medication_score ms ON ms.patient_id = p.id
LEFT JOIN lab_score ls ON ls.patient_id = p.id
)
SELECT
patient_id,
name,
mrn,
insurance_type,
age_score,
admission_score,
diag_score,
med_score,
lab_score,
total_risk_score,
NTILE(4) OVER (ORDER BY total_risk_score DESC) AS risk_quartile,
RANK() OVER (ORDER BY total_risk_score DESC) AS risk_rank
FROM composite
ORDER BY total_risk_score DESC
LIMIT 30;
What This Returns
patient_id | name | mrn | total_risk_score | risk_quartile | risk_rank
-----------+------------+----------+------------------+---------------+----------
261 | Patient 261| MRN00261 | 42 | 1 | 1
391 | Patient 391| MRN00391 | 42 | 1 | 1
11 | Patient 11 | MRN00011 | 41 | 1 | 3
...
2. Risk Tier Distribution
WITH scores AS (
-- (same CTE chain as above, omitted for brevity)
SELECT
p.id AS patient_id,
COALESCE((DATE_PART('year', AGE(p.dob)) - 50) / 10 * 1, 0)::INT AS total_risk_score
FROM hc_patients p
),
tiers AS (
SELECT
patient_id,
CASE
WHEN total_risk_score >= 30 THEN 'High Risk (30+)'
WHEN total_risk_score >= 15 THEN 'Medium Risk (15-29)'
WHEN total_risk_score >= 5 THEN 'Low Risk (5-14)'
ELSE 'Minimal Risk (<5)'
END AS risk_tier
FROM scores
)
SELECT risk_tier, COUNT(*) AS patients,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM tiers
GROUP BY risk_tier
ORDER BY MIN(
CASE risk_tier
WHEN 'High Risk (30+)' THEN 1
WHEN 'Medium Risk (15-29)' THEN 2
WHEN 'Low Risk (5-14)' THEN 3
ELSE 4
END
);
3. Department-Level Readmission Rates
WITH readmits AS (
SELECT DISTINCT a1.ward_id, a1.id AS original_id
FROM hc_admissions a1
JOIN hc_admissions a2
ON a2.patient_id = a1.patient_id
AND a2.id != a1.id
AND a2.admitted_at > a1.discharged_at
AND a2.admitted_at <= a1.discharged_at + INTERVAL '30 days'
WHERE a1.discharged_at IS NOT NULL
)
SELECT
w.name,
COUNT(a.id) AS discharges,
COUNT(r.original_id) AS readmissions,
ROUND(100.0 * COUNT(r.original_id) / NULLIF(COUNT(a.id), 0), 1) AS readmit_rate_pct,
RANK() OVER (ORDER BY
100.0 * COUNT(r.original_id) / NULLIF(COUNT(a.id), 0) DESC
) AS ward_rank
FROM hc_admissions a
JOIN hc_wards w ON w.id = a.ward_id
LEFT JOIN readmits r ON r.original_id = a.id AND r.ward_id = a.ward_id
WHERE a.discharged_at IS NOT NULL
GROUP BY w.name
ORDER BY readmit_rate_pct DESC;
Key Takeaway
Multi-CTE risk scoring is a powerful and readable pattern. Each CTE computes one signal; the final composite CTE aggregates them. Using COALESCE(value, 0) handles patients with no admissions/labs without dropping them from the result. NTILE(4) divides patients into risk quartiles automatically regardless of the score distribution — no manual threshold-setting required. This architecture translates directly to clinical decision support systems.