CTEs & Window Functions SQL practice problemHardVerified answer

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_idINTEGER
  • first_nameVARCHAR(50)
  • last_nameVARCHAR(50)
  • salaryINTEGER
  • department_idINTEGER

departments

  • department_idINTEGER
  • department_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_namefirst_namelast_namesalarydept_mediandiff_from_medianpercentile_rank
EngineeringMichaelScott18000011000070000100
EngineeringDavidMartinez1300001100002000080
EngineeringEmilyChen1250001100001500060
EngineeringRobertWilliams95000110000-1500040
EngineeringLisaAnderson90000110000-2000020
EngineeringAmandaWhite75000110000-350000
ExecutiveJenniferKing25000025000000
FinanceJessicaMoore1050008750017500100
FinanceDanielLee7000087500-175000
Human ResourcesPatriciaDavis950008000015000100

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: