N059-M3 Tier 5 · Expert · medium hr · Helix Systems

Return each department name and the number of salary records on file for active employees in that department

Part of Join Fanout and Aggregate Correctness in SQL

The problem

Scenario: Helix Systems' HR analytics team is investigating compensation record volume per department, knowing each active employee can have multiple salary records due to pay changes over time.

Task: Write a query to return each department name and the number of salary records on file for active employees in that department.

Assumptions:

  • An active employee has is_active equal to TRUE.
  • An employee can have multiple salary records — each pay change adds a new record.
  • The result covers only salary records associated with active employees.

Output:

  • One row per department with at least one active employee on file.
  • Columns in this order: department_name, salary_record_count.
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
SELECT
  d.name AS department_name,
  COUNT(*) AS salary_record_count
FROM
  employees e
  JOIN departments d ON e.department_id = d.id
  JOIN salaries s ON s.employee_id = e.id
WHERE
  e.is_active = TRUE
GROUP BY
  d.name

The shape

Each active employee has multiple salary records, so joining employees to salaries produces one row per salary record, not per employee. COUNT(*) then totals those rows inside each department — which is exactly the salary-record count the HR analytics team is investigating.

Clause by clause

  • SELECT d.name AS department_name, COUNT(*) AS salary_record_count returns the department name and the salary-record count inside that department. COUNT(*) here counts every row of the joined result, which after the join is one row per salary record.
  • FROM employees e reads the employees as the driving table. is_active lives here, which is why the activity filter applies to this side.
  • JOIN departments d ON e.department_id = d.id attaches each employee's department. This is a one-to-one lookup — every employee has one department — so this join does not fan out.
  • JOIN salaries s ON s.employee_id = e.id attaches each employee's salary records. This is the one-to-many side: an employee with four pay changes contributes four rows.
  • WHERE e.is_active = TRUE keeps only active employees. The filter applies on the employee side before the salary records get aggregated, so an inactive employee's salary history doesn't enter the count.
  • GROUP BY d.name collapses the joined rows down to one per department, with the count running inside each group.

Why COUNT(*) and not COUNT(DISTINCT e.id)

COUNT(DISTINCT e.id) would return the number of distinct employees per department, which is a different metric — the prompt asks for the number of salary records, not the number of employees. Salary-record count is exactly what COUNT(*) on the post-join, post-filter row set produces, because every row in that set is one salary record. The fanout that an employee-count question would have to guard against is the very shape this question is asking for.

You practiced counting child rows under a per-parent restriction — every active employee's salary history contributes its full record count to the department total.

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.