The GROUP BY clause collapses multiple rows that share the same value in one or more columns into a single summary row, so you can run an aggregate function (like SUM or COUNT) separately for each group instead of across the whole table. It's the clause behind almost every "totals by category" report you've ever seen.

Without GROUP BY, an aggregate function like SUM(salary) collapses the entire table into one number. With it, you get one number per distinct value in the grouped column — total salary per department, order count per customer, and so on.

Basic GROUP BY Syntax

Using the running employees table (id, name, department, salary, hire_date), here's the total salary paid out per department:

SELECT department, SUM(salary) AS total_salary
FROM employees
GROUP BY department;

Result:

department | total_salary
-------------|-------------
Engineering | 278000
Marketing | 115000
Sales | 191000

SQL scans every row, buckets them by department, then applies SUM() within each bucket. You can group by more than one column too — GROUP BY department, hire_date would create a separate group for every unique department-and-date combination.

The Golden Rule: Select What You Group By

Every column in your SELECT list must either be inside an aggregate function or listed in GROUP BY. This query fails in most databases:

-- Invalid: 'name' is neither aggregated nor grouped
SELECT department, name, SUM(salary)
FROM employees
GROUP BY department;

The database has no way to know which employee's name to show for a department with multiple employees in it — grouping is lossy by nature, so SQL refuses to guess.

Grouping With a Filter

You can still use a WHERE clause before grouping, to filter individual rows out of the calculation entirely:

SELECT department, COUNT(*) AS headcount
FROM employees
WHERE hire_date >= '2020-01-01'
GROUP BY department;

Result:

department | headcount
-------------|----------
Engineering | 2
Marketing | 2
Sales | 1

Once you need to filter groups after they've been aggregated — for example, only departments with a headcount above some number — that's a job for HAVING instead of WHERE, since WHERE runs before grouping happens.