N062-H3 Tier 5 · Expert · hard hr · Helix Systems

Return each `department_name`, its `dept_total` (combined current salary), and its `salary_share` (its share of the company-wide current salary expenditure expressed as a percentage)

Part of Choosing Between Subqueries, CTEs, and Joins in SQL

The problem

Scenario: Helix Systems' finance team needs a department salary report showing each department's name, its current salary expenditure, and its share of the company-wide current salary expenditure.

Task: Write a query to return each department_name, its dept_total (combined current salary), and its salary_share (its share of the company-wide current salary expenditure expressed as a percentage).

Assumptions:

  • A current salary record has end_date recorded as a missing value.
  • A department's dept_total is the combined current salary amount across its employees.
  • A department's salary_share is dept_total divided by the combined current salary across every department in the result, multiplied by 100.
  • The result covers only departments with at least one current salary record.

Output:

  • One row per qualifying department.
  • Columns in this order: department_name, dept_total, salary_share.
Schema · hr 4 tables
departments
id integer
name text
location text
budget numeric
salaries
id integer
employee_id integer
amount numeric
effective_date date
end_date? date
employees
id integer
name text
email text
department_id integer
manager_id? integer
hire_date date
title text
is_active boolean
job_history
id integer
employee_id integer
title text
department_id integer
start_date date
end_date? date

Run previews · Check grades

Write a query, then run it to see results here.

Worked solution Try it yourself first
Solution query
WITH
  dept_salary_totals AS (
    SELECT
      e.department_id,
      SUM(s.amount) AS dept_total
    FROM
      employees e
      JOIN salaries s ON s.employee_id = e.id
    WHERE
      s.end_date IS NULL
    GROUP BY
      e.department_id
  ),
  dept_shares AS (
    SELECT
      department_id,
      dept_total,
      dept_total * 100.0 / SUM(dept_total) OVER () AS salary_share
    FROM
      dept_salary_totals
  )
SELECT
  d.name AS department_name,
  ds.dept_total,
  ds.salary_share
FROM
  dept_shares ds
  JOIN departments d ON d.id = ds.department_id

The shape

Two CTEs and a window function carry the work. The first CTE totals current salaries by department; the second uses SUM(dept_total) OVER () to attach the company-wide total to every department row in one pass. The outer query joins to departments for the name.

Clause by clause

  • WITH dept_salary_totals AS (SELECT e.department_id, SUM(s.amount) AS dept_total FROM employees e JOIN salaries s ON s.employee_id = e.id WHERE s.end_date IS NULL GROUP BY e.department_id) produces one row per department that has at least one current salary record. The WHERE s.end_date IS NULL filter restricts to current salaries before the aggregation, and the inner JOIN excludes departments with no qualifying record — matching the prompt's contract.
  • dept_shares AS (SELECT department_id, dept_total, dept_total * 100.0 / SUM(dept_total) OVER () AS salary_share FROM dept_salary_totals) is where the window function does its work. SUM(dept_total) OVER () with an empty window frame sums dept_total across every row in dept_salary_totals and attaches that single value to each row. The division then computes each department's share against the same denominator.
  • SELECT d.name AS department_name, ds.dept_total, ds.salary_share FROM dept_shares ds JOIN departments d ON d.id = ds.department_id joins the department name in. The inner JOIN is safe because every row in dept_shares came from a department that has a current salary; the join just attaches the label.

Why SUM(...) OVER () and not a scalar subquery

SELECT department_id, dept_total, dept_total * 100.0 / (SELECT SUM(dept_total) FROM dept_salary_totals) AS salary_share FROM dept_salary_totals produces the same numbers. The window form pulls the company-wide total into the same scan rather than running a separate aggregation pass. On a small department table the difference is irrelevant; the readability is comparable; pick either.

The trap

The denominator is "the combined current salary across every department in the result," not "across every salary record." Those are the same number here because the CTE is built only from current salaries, but the wording is what matters if anything ever filters dept_shares further. SUM(dept_total) OVER () always sums across every row of the CTE before any outer filter — that is what makes the shares add to exactly 100 here. A common related mistake is writing 100 instead of 100.0 in the multiplier: integer arithmetic would truncate any fractional percentage to a whole-number share, and most of the rows would end up as 0 or low-precision integers instead of the intended decimals.

You practiced layering two CTEs — first per-department totals, then a window-function denominator over them — before pairing each department with its name from a third source.

How you actually get good at SQL

Reading explains SQL. Writing it, over and over with instant feedback, is what makes you fluent.

That's the whole SQLMaxx loop: 600+ real problems, instant AI feedback, mastery you can actually see, and spaced review that won't let you forget.

A stack of SQL practice problem cards, the top card showing an employees table.
615 problems · 66 concepts

Real problems. Not toy examples.

615 hand-built problems spanning all 66 concepts, from basic SELECTs to window functions, built on real schemas and real business questions, the kind you'll actually get asked on the job. Enough reps to make SQL automatic.

A retro computer showing a SQL query marked correct with a green checkmark.
Instant AI feedback

Write a query. Know if it's right in one second.

No copying an answer and hoping it clicked. The AI grader checks your real query against real data, catches exactly what's wrong, and explains the fix in plain English, like a senior analyst reading over your shoulder on every problem.

A circular mastery progress dial filling from blue to green, the SQLMaxx diamond at its center.
Mastery tracking

Stop guessing whether you actually know it.

SQLMaxx tracks every concept and shows you what you've mastered and what's still shaky. Your skills fill in one concept at a time, so 'I think I get joins' becomes something you can prove.

A SQL query editor circled by a blue return arrow with a clock, scheduled to come back for review.
Spaced review

Learn it once. Keep it for good.

Most of what you learn this week fades by next week. So when a concept comes due for review, SQLMaxx hands you a fresh problem to solve from a blank editor, not a flashcard to re-read. A research-backed spaced-repetition algorithm (FSRS) times each return for right before you'd forget, so your SQL is still there months later, when the interview or the job actually needs it.

Practice, feedback, mastery, review. That's the loop that turns reading into real skill.

Start free

No account, no credit card. Start solving in under a minute.