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
SELECT department, COUNT(*) AS employee_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
Returns 2 rows: ('Engineering', 2, 68000.00), ('Sales', 1, 55000.00)
Using HAVING to filter groups after aggregation
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 1;
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
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 andSUM(salary)totals them.- A condition on a group goes in
HAVING, afterGROUP BY.
Show 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.
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