Approach (brief): Define cohorts by hire quarter, compute for each hire-cohort the number/percent of employees active in each following quarter (survival/retention), and compute promotion metrics (time-to-first-promotion distribution, median promotion lag, promotion rate by cohort). Use SQL to build a cohort table, then derive retention and promotion-lag tables. Visualize with a cohort heatmap (hire-quarter rows × months/quarters since hire columns) showing retention% with annotations and trend lines for promotion medians.SQL: create cohort membership and base tablesql
-- 1. Base employee events: hires, status by quarter, promotions
WITH hires AS (
SELECT employee_id,
DATE_TRUNC('quarter', hire_date) AS hire_quarter,
hire_date
FROM employees
WHERE hire_date IS NOT NULL
),
status_by_q AS (
-- snapshot per quarter: active if not terminated before quarter end
SELECT e.employee_id,
h.hire_quarter,
DATE_TRUNC('quarter', d.quarter_start) AS quarter,
CASE WHEN (t.termination_date IS NULL OR t.termination_date >= d.quarter_end) THEN 1 ELSE 0 END AS active
FROM hires h
JOIN (
-- generate quarter ranges covering analysis window
SELECT generate_series(min(hire_date)::date, current_date, interval '3 month') AS quarter_start
FROM hires
) d ON 1=1
JOIN employees e ON e.employee_id = h.employee_id
LEFT JOIN employees t ON t.employee_id = e.employee_id
WHERE d.quarter_start >= h.hire_quarter
),
promotions AS (
SELECT employee_id,
MIN(promotion_date) AS first_promo_date
FROM promotions_table
GROUP BY employee_id
)
SELECT h.employee_id, h.hire_quarter, s.quarter,
DATE_PART('quarter', AGE(s.quarter, h.hire_quarter)) + 4*(DATE_PART('year', s.quarter) - DATE_PART('year', h.hire_quarter)) AS quarters_since_hire,
s.active,
p.first_promo_date,
CASE WHEN p.first_promo_date IS NOT NULL THEN DATE_PART('day', p.first_promo_date - h.hire_date) ELSE NULL END AS days_to_promo
FROM hires h
JOIN status_by_q s ON s.employee_id = h.employee_id
LEFT JOIN promotions p ON p.employee_id = h.employee_id;
Build retention cohort matrixsql
WITH cohort_events AS ( ...use previous query as CTE... )
SELECT hire_quarter,
quarters_since_hire,
COUNT(*) FILTER (WHERE active=1) AS active_count,
COUNT(*) AS cohort_size,
ROUND(100.0 * SUM(active)::numeric / COUNT(*),2) AS retention_pct
FROM cohort_events
GROUP BY hire_quarter, quarters_since_hire
ORDER BY hire_quarter, quarters_since_hire;
Build promotion-lag summary per cohortsql
SELECT hire_quarter,
COUNT(first_promo_date) AS promoted_count,
COUNT(*) AS cohort_size,
ROUND(100.0 * COUNT(first_promo_date)/COUNT(*),2) AS promotion_rate_pct,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_to_promo) AS median_days_to_promo
FROM (
SELECT DISTINCT employee_id, hire_quarter, first_promo_date, days_to_promo
FROM cohort_events
) t
GROUP BY hire_quarter
ORDER BY hire_quarter;
Key outputs to create:- Retention (survival) table: matrix with hire_quarter × quarters_since_hire showing retention% and counts.- Cumulative retention curve per cohort (line chart) for 1,2,3+ years.- Promotion-lag table: promotion_rate_pct and median_days_to_promo by cohort; distribution histogram or Kaplan–Meier for time-to-promotion.- Table of attrition reasons and role/level breakdowns to contextualize retention.Visualization recommendation (best for leadership): Cohort heatmap- Rows = hire_quarters (oldest top), Columns = quarters since hire (0,1,2,... up to 12)- Cell value = retention% with diverging color scale (e.g., dark = high retention)- Annotate cells with % and cohort size; include a small line chart inset showing median days-to-promotion trend by cohort.Why: heatmap quickly highlights which hire cohorts underperform on retention across time and correlates visually with promotion trends when paired with the inset.