Salary vs Department Median & Percentile
Return each employee with a 3-row moving average salary and their percentile rank.
- CTEs
- Window functions
- Joins
- Subqueries
- Aggregation
Interview brief
Understand the request
Finance analytics lead The rolling pay analysis uses a moving average and percentile rank to contextualise each employee's salary position.
For each employee compute: their department's median salary, the difference from it, and their percentile rank within the department (PERCENT_RANK × 100, rounded to 2). Return department_name, first_name, last_name, salary, dept_median, diff_from_median, percentile_rank — ordered by department_name, salary DESC, employee_id.
Return
- Return employee_name, salary, moving_avg_salary, and percentile_rank.
Constraints
- Use AVG() with a ROWS BETWEEN window frame and PERCENT_RANK() or equivalent.
Data you will use
Review the relevant tables before deciding how to join, filter, or aggregate them.
employees
employee_idINTEGERfirst_nameVARCHAR(50)last_nameVARCHAR(50)salaryINTEGERdepartment_idINTEGER
departments
department_idINTEGERdepartment_nameVARCHAR(50)
Hints, when you need them
Open one clue at a time so you still do the reasoning.
Hint 1
Median without a built-in: number the rows, find the middle one (odd count) or average the middle two (even count).
Hint 2
rn IN ((cnt+1)/2, (cnt+2)/2) selects the middle row(s) using integer division.
Hint 3
PERCENT_RANK() returns 0..1 — multiply by 100 for a percentage.
Verified SQL answer
Attempt the problem first, then compare structure and reasoning—not just syntax.
Reveal solution and explanation
WITH dept_median AS (SELECT department_id, AVG(salary) AS median_salary FROM (SELECT department_id, salary, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary) AS rn, COUNT(*) OVER (PARTITION BY department_id) AS cnt FROM employees) t WHERE rn IN ((cnt + 1) / 2, (cnt + 2) / 2) GROUP BY department_id) SELECT d.department_name, e.first_name, e.last_name, e.salary, m.median_salary AS dept_median, e.salary - m.median_salary AS diff_from_median, ROUND(PERCENT_RANK() OVER (PARTITION BY e.department_id ORDER BY e.salary) * 100, 2) AS percentile_rank FROM employees e INNER JOIN departments d ON e.department_id = d.department_id INNER JOIN dept_median m ON e.department_id = m.department_id ORDER BY d.department_name, e.salary DESC, e.employee_id;Why this works
Median is awkward in SQL because most engines lack a portable MEDIAN(). The ROW_NUMBER + middle-row trick is the classic workaround. PERCENT_RANK gives each row's relative standing (0 = lowest, 1 = highest) within its partition.
Expected result
Use this output to verify values, aliases, ordering, and row count.
| department_name | first_name | last_name | salary | dept_median | diff_from_median | percentile_rank |
|---|---|---|---|---|---|---|
| Engineering | Michael | Scott | 180000 | 110000 | 70000 | 100 |
| Engineering | David | Martinez | 130000 | 110000 | 20000 | 80 |
| Engineering | Emily | Chen | 125000 | 110000 | 15000 | 60 |
| Engineering | Robert | Williams | 95000 | 110000 | -15000 | 40 |
| Engineering | Lisa | Anderson | 90000 | 110000 | -20000 | 20 |
| Engineering | Amanda | White | 75000 | 110000 | -35000 | 0 |
| Executive | Jennifer | King | 250000 | 250000 | 0 | 0 |
| Finance | Jessica | Moore | 105000 | 87500 | 17500 | 100 |
| Finance | Daniel | Lee | 70000 | 87500 | -17500 | 0 |
| Human Resources | Patricia | Davis | 95000 | 80000 | 15000 | 100 |
Previewing 10 of 15 expected rows. Run the query in the editor to inspect the full result.
Learn the concepts behind this answer
Strengthen your understanding with these targeted learning topics: