Admissions Analysis — Volume, Length of Stay, and Readmissions
Admissions Analysis — Volume, Length of Stay, and Readmissions
Understanding admission patterns is the foundation of hospital operations. This lesson covers four essential queries: daily volume trends, length-of-stay distributions, 30-day readmission detection, and frequent-flyer identification.
1. Daily Admission Volume Trends
Track how many patients are admitted each day. Use DATE_TRUNC to group timestamps into day buckets.
SELECT
DATE_TRUNC('day', admitted_at)::DATE AS admission_date,
COUNT(*) AS admissions,
COUNT(*) FILTER (WHERE admission_type = 'Emergency') AS emergency,
COUNT(*) FILTER (WHERE admission_type = 'Elective') AS elective,
COUNT(*) FILTER (WHERE admission_type = 'Transfer') AS transfer
FROM hc_admissions
WHERE admitted_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY 1
ORDER BY 1;
For a 7-day rolling average to smooth noise:
SELECT
admission_date,
admissions,
ROUND(AVG(admissions) OVER (
ORDER BY admission_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 1) AS rolling_7d_avg
FROM (
SELECT
DATE_TRUNC('day', admitted_at)::DATE AS admission_date,
COUNT(*) AS admissions
FROM hc_admissions
GROUP BY 1
) daily
ORDER BY admission_date;
What This Returns
admission_date | admissions | rolling_7d_avg
---------------+------------+---------------
2023-01-01 | 3 | 3.0
2023-01-02 | 4 | 3.5
2023-01-03 | 3 | 3.3
...
2. Average and Median Length of Stay by Ward
Length of stay (LOS) is a key efficiency metric. Use PERCENTILE_CONT for median — it is more robust than AVG because a single 60-day outlier can skew the average significantly.
SELECT
w.name AS ward,
COUNT(a.id) AS total_admissions,
ROUND(AVG(
EXTRACT(EPOCH FROM (a.discharged_at - a.admitted_at)) / 86400
), 1) AS avg_los_days,
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (a.discharged_at - a.admitted_at)) / 86400
)::NUMERIC, 1) AS median_los_days,
ROUND(PERCENTILE_CONT(0.9) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (a.discharged_at - a.admitted_at)) / 86400
)::NUMERIC, 1) AS p90_los_days,
COUNT(*) FILTER (WHERE a.discharged_at IS NULL) AS currently_admitted
FROM hc_admissions a
JOIN hc_wards w ON w.id = a.ward_id
GROUP BY w.name
ORDER BY avg_los_days DESC;
What This Returns
ward | total_admissions | avg_los_days | median_los_days | p90_los_days | currently_admitted
-----------+------------------+--------------+-----------------+--------------+-------------------
ICU | 116 | 7.8 | 7.5 | 13.4 | 12
Surgery | 116 | 7.6 | 7.5 | 13.0 | 12
General-A | 116 | 7.4 | 7.0 | 13.0 | 12
...
3. Detecting 30-Day Readmissions
A readmission within 30 days of discharge is a quality-of-care indicator and often penalised by payers. Detect these with a self-join:
SELECT
a1.id AS original_admission_id,
a1.patient_id,
a1.discharged_at::DATE AS discharge_date,
a2.id AS readmission_id,
a2.admitted_at::DATE AS readmit_date,
(a2.admitted_at - a1.discharged_at) AS gap,
a2.admission_type
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
ORDER BY a1.patient_id, a1.discharged_at;
Summarise the 30-day readmission rate by ward:
WITH readmits AS (
SELECT DISTINCT a1.id AS original_id, a1.ward_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
FROM hc_admissions a
JOIN hc_wards w ON w.id = a.ward_id
LEFT JOIN readmits r ON r.original_id = a.id
WHERE a.discharged_at IS NOT NULL
GROUP BY w.name
ORDER BY readmit_rate_pct DESC;
What This Returns
ward | discharges | readmissions | readmit_rate_pct
-----------+------------+--------------+------------------
ICU | 104 | 42 | 40.4
Pediatric | 101 | 41 | 40.6
...
4. Frequent Admitters — Patients with 3+ Admissions in 12 Months
High-frequency admitters often signal care gaps or complex chronic conditions:
SELECT
p.id,
p.name,
p.mrn,
p.insurance_type,
COUNT(a.id) AS admissions_12mo,
MIN(a.admitted_at)::DATE AS first_admission,
MAX(a.admitted_at)::DATE AS last_admission,
STRING_AGG(DISTINCT d.description, ', ' ORDER BY d.description)
AS primary_diagnoses
FROM hc_patients p
JOIN hc_admissions a
ON a.patient_id = p.id
AND a.admitted_at >= CURRENT_DATE - INTERVAL '12 months'
LEFT JOIN hc_diagnoses d
ON d.admission_id = a.id
AND d.is_primary = true
GROUP BY p.id, p.name, p.mrn, p.insurance_type
HAVING COUNT(a.id) >= 3
ORDER BY admissions_12mo DESC;
What This Returns
id | name | mrn | insurance_type | admissions_12mo
----+------------+-----------+----------------+----------------
12 | Patient 12 | MRN00012 | Medicare | 6
24 | Patient 24 | MRN00024 | Medicaid | 5
...
Key Takeaway
Use DATE_TRUNC + COUNT for volume trends, PERCENTILE_CONT for robust LOS statistics, and a self-join on patient_id with a date-range condition to detect readmissions. The self-join pattern is the standard technique for any "event within N days of another event" problem and appears in churn, fraud, and retention analysis as well.