Course topics

By WebNest Studio Engineering Team

Database and SQL Tutorial

Grouping Data with GROUP BY

GROUP BY collapses multiple rows sharing the same value in one or more columns into a single summary row per group, typically paired with aggregate functions like COUNT, SUM, and AVG to compute a statistic for each group. This is the foundation of virtually every reporting and analytics query: totals by category, counts by status, averages by region.

Why it matters

Reports are questions about groups: sales per month, average salary per department, number of orders per customer. GROUP BY with aggregate functions answers them in one query. The difference between WHERE and HAVING is one of the most frequently asked SQL interview questions.

Aggregate Functions

Aggregate functions compute a single value from a set of rows.

  • COUNT(*) — number of rows in the group; COUNT(column) counts non-NULL values only.
  • SUM(column) — total of a numeric column across the group.
  • AVG(column) — average of a numeric column across the group.
  • MIN(column) / MAX(column) — smallest and largest value in the group.

How GROUP BY Works

GROUP BY column partitions the rows into groups sharing the same value in that column, and every column named in the SELECT list must either appear in the GROUP BY clause or be wrapped in an aggregate function — a database will reject a query that mixes a raw, non-aggregated column with GROUP BY unless it is one of the grouping columns.

HAVING vs. WHERE

WHERE filters individual rows before grouping happens, while HAVING filters entire groups after aggregation, based on the result of an aggregate function. You cannot write WHERE COUNT(*) > 5 because COUNT does not exist yet at the row-filtering stage — that condition belongs in HAVING, applied after the grouping and aggregation are computed.

Examples

Counting and summing per department

SQL
SELECT department, COUNT(*) AS employee_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
Output
Returns 2 rows: ('Engineering', 2, 68000.00), ('Sales', 1, 55000.00)

Using HAVING to filter groups after aggregation

SQL
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 1;
Output
Returns 1 row: ('Engineering', 2) — Sales is excluded because its count of 1 does not satisfy HAVING COUNT(*) > 1.

Common Mistakes

  • Using WHERE to try to filter on an aggregate value (e.g., "WHERE COUNT(*) > 5") instead of HAVING, which causes a syntax/semantic error.
  • Selecting a raw column that is not in the GROUP BY clause and not wrapped in an aggregate function, leading to an error or (in more permissive databases) an arbitrary, unpredictable value from the group.
  • Confusing COUNT(*) (counts all rows) with COUNT(column) (counts only non-NULL values in that column) and getting different totals than expected.
  • Applying HAVING conditions that could have been applied earlier as WHERE, which forces the database to aggregate rows that could have been filtered out first, hurting performance.

Exercise

For each department, return the department, the number of staff as staff and the total salary as total, but only for departments that have at least two employees.

Expected output: department | staff | total then IT | 2 | 110000

SQL · starter code
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary INTEGER);
INSERT INTO employees (id, name, department, salary) VALUES
  (1, 'Asha', 'IT', 50000),
  (2, 'Ravi', 'IT', 60000),
  (3, 'Zoya', 'HR', 70000);

-- TODO: department, number of staff and total salary, for departments with 2 or more staff
Show hints
  • COUNT(*) counts the rows of each group and SUM(salary) totals them.
  • A condition on a group goes in HAVING, after GROUP BY.
Show solution
SQL · solution
CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT, department TEXT, salary INTEGER);
INSERT INTO employees (id, name, department, salary) VALUES
  (1, 'Asha', 'IT', 50000),
  (2, 'Ravi', 'IT', 60000),
  (3, 'Zoya', 'HR', 70000);

SELECT department, COUNT(*) AS staff, SUM(salary) AS total
FROM employees
GROUP BY department
HAVING COUNT(*) >= 2;

Quick quiz

1. What is the difference between COUNT(*) and COUNT(email)?

  • A. There is none
  • B. COUNT(*) counts all rows; COUNT(email) counts only rows where email is not null
  • C. COUNT(email) counts distinct emails
  • D. COUNT(*) ignores duplicates
Show answer

B. Aggregate functions other than COUNT(*) skip NULL values.

2. Which clause filters groups after aggregation?

  • A. WHERE
  • B. ORDER BY
  • C. HAVING
  • D. LIMIT
Show answer

C. WHERE runs before grouping and cannot use aggregate functions.

3. In a query with GROUP BY department, which columns may appear in the SELECT list?

  • A. The grouped columns and aggregate functions
  • B. Any column of the table
  • C. Only aggregate functions
  • D. Only the primary key
Show answer

A. A column that is neither grouped nor aggregated has no single value for the group.

Interview questions

In what order are the clauses of a SELECT statement processed?

Show answer

Logically: FROM and the joins build the rows; WHERE filters them; GROUP BY forms groups; HAVING filters the groups; SELECT computes the output columns; DISTINCT removes duplicates; ORDER BY sorts; and LIMIT cuts the result. This order explains why a column alias defined in SELECT cannot be used in WHERE, and why aggregates are not allowed there.

How would you find duplicate values in a column?

Show answer

Group by the column and keep the groups with more than one row: SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1. The result lists each duplicated value and how many times it occurs. To see the full rows, join that result back to the table or use a window function such as ROW_NUMBER() partitioned by the column.

Key Points to Remember

  • GROUP BY collapses rows sharing the same value(s) into one summary row per group, used with aggregate functions like COUNT, SUM, and AVG.
  • Every non-aggregated column in SELECT must appear in the GROUP BY clause.
  • WHERE filters rows before grouping; HAVING filters groups after aggregation.
  • Filter as much as possible with WHERE before grouping, and reserve HAVING for conditions on the aggregated result itself.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering