N048-M4 Tier 4 · Advanced · medium hr · Helix Systems

Return every employee's ID, name, total number of salary records on file, and highest salary amount on record

Part of LATERAL Joins in SQL

The problem

Helix Systems' HR compliance team is reviewing salary records for data quality purposes.

Write a query to return every employee's ID, name, total number of salary records on file, and highest salary amount on record.

Assumptions:

  • An employee's salary-record count is the number of salaries records linked to that employee_id. The highest salary is the largest amount across those records.
  • Every employee must appear in the result, including employees with no salary records on file. Employees with no salary records should show a count of 0 and a missing highest-salary value.

Output:

  • One row per employee, with columns id, name, salary_count, and max_salary.
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
  e.id,
  e.name,
  sal_stats.salary_count,
  sal_stats.max_salary
FROM
  employees e
  CROSS JOIN LATERAL (
    SELECT
      COUNT(*) AS salary_count,
      MAX(amount) AS max_salary
    FROM
      salaries s
    WHERE
      s.employee_id = e.id
  ) sal_stats

The shape

The lateral runs per employee and reduces that employee's salary records to one stats row. No GROUP BY inside the aggregate means the lateral always returns one row per employee, which is what keeps every employee, including those with no salary records on file, in the compliance review.

Clause by clause

  • SELECT e.id, e.name, sal_stats.salary_count, sal_stats.max_salary returns the employee's identity from the outer table and the two aggregate stats from the lateral.
  • FROM employees e is the driving table. Every row will be paired with exactly one lateral row, so every employee survives to the output.
  • CROSS JOIN LATERAL ( SELECT COUNT(*) AS salary_count, MAX(amount) AS max_salary FROM salaries s WHERE s.employee_id = e.id ) sal_stats runs once per employee. Because the aggregate has no GROUP BY, it returns one row regardless of whether the inner filter matches anything. For an employee with no salary records, COUNT(*) is 0 and MAX(amount) is NULL, which is the missing max-salary value the prompt asks for. CROSS JOIN then keeps every employee because the lateral always has one row to pair.

Why this and not a scalar correlated subquery in SELECT

A correlated subquery in SELECT can return one scalar, so you could pull salary_count and max_salary as two separate correlated subqueries, one per column. That works on this exact shape. The lateral form computes both aggregates from a single scan of salaries per employee instead of two; on a large salaries table that is the difference between one pass and two. The lateral also keeps related output columns together as a single derived value, which reads more naturally.

The trap

The "missing max-salary" requirement is what forces the aggregate to live inside the lateral with no GROUP BY. Write the lateral as SELECT amount FROM salaries WHERE s.employee_id = e.id and aggregate outside with MAX(sal_stats.amount) and the employees with no salary records vanish, because the lateral returns zero rows and CROSS JOIN drops the outer employee. Aggregate-inside-the-lateral is the shape that preserves every outer row under CROSS JOIN LATERAL.

You practiced CROSS JOIN LATERAL with COUNT and MAX in the inner aggregate — every parent record is preserved; COUNT returns 0 and MAX returns missing when no children match.

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.