Helix Systems' HR team wants to review every salary record on file.
Write a query to return one row per salary record, showing the employee's name alongside the salary amount and end date for that record.
Assumptions:
- The
salariestable contains every salary record ever held by an employee — current and historical. An employee with three salary changes over their tenure has three rows here. - The
employeestable contains every active and former employee at Helix Systems, identified byemployees.id. - The result row count exceeds the employee headcount because of the one-to-many relationship between employees and salary records.
Output:
- One row per salary record, with columns
employee_name,amount, andend_date.
Schema · hr 4 tables
Run previews · Check grades
Write a query, then run it to see results here.
Worked solution Try it yourself first
SELECT
e.name AS employee_name,
s.amount,
s.end_date
FROM
salaries s
JOIN employees e ON s.employee_id = e.id The shape
One employee can hold many salary records, so the result has one row per salary record, not one row per employee. Sarah Chen appears twice (her 380000 and her 420000), Marcus Reid appears twice, and so on. The join multiplies the employee's row across however many salary records they have, with the employee's name duplicated on each.
Clause by clause
FROM salaries sreads from the salaries table, the many side of the relationship. Each row is one salary record, with anemployee_idlinking back to the person who held it.JOIN employees e ON s.employee_id = e.idpairs each salary row with its employee. For every row insalaries, PostgreSQL finds the matching row inemployeesand attaches the employee's columns. An employee with three salary records gets paired three times — one pairing per salary row — and the employee'snameappears three times in the result.SELECT e.name AS employee_name, s.amount, s.end_datereturns the employee's name from the employees side and the salary record's amount and end date from the salaries side. The row count of the result matches the row count ofsalaries, not the headcount ofemployees.
Why this and not the other direction
The choice of which table to put in FROM versus JOIN doesn't change the result for an inner join. The conventional pattern is to put the many side first when the report is about "every salary record," because the FROM table's row count is the report's row count. Reading the query top-down, FROM salaries immediately signals that the answer is one row per salary record.
The trap
The trap is reading the result row count as if it were the employee count. A row count of 137 doesn't mean Helix has 137 employees. It means there are 137 salary records on file, spread across however many distinct employees. An employee with several historical raises contributes multiple rows, and their name repeats on each.
The rule: when a join multiplies, the result row count is the count of the many side, not the count of the one side. Mistaking the row count for a headcount is the standard one-to-many error, and it's silent. The query runs cleanly and the number looks plausible until you check it against the known employee total.
You practiced an INNER JOIN where the multiplying side is the fact table (salaries) and the parent (employee) appears repeated across multiple rows. The recurring shape: a one-to-many join always produces one output row per fact-table row, with the parent's columns duplicated as needed.