Q1: Why does this GROUP BY query fail?

ungrouped-column.sql
SELECT customer_id, order_date, COUNT(*)
FROM orders
GROUP BY customer_id;
-- Error: column "order_date" must appear in GROUP BY
-- or be used in an aggregate function
Common guess (picks any row's date)
customer_idorder_datecount
101Jan 34
Actual behavior
Result
Query fails to run at all

The database doesn't pick an arbitrary row's order_date for you — it refuses to run the query. Once rows are collapsed by customer_id, order_date has no single well-defined value per group unless it's also grouped or wrapped in an aggregate like MAX(order_date). Full explanation in SQL GROUP BY Without Aggregate Function.

Q2: WHERE and HAVING in the same query

where-and-having.sql
SELECT customer_id, COUNT(*) AS n
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING COUNT(*) > 3;

WHERE removes non-completed orders before any counting happens. GROUP BY then collapses what's left into one row per customer. HAVING filters those already-grouped rows down to customers with more than 3 completed orders. Swapping the two — putting status = 'completed' in HAVING instead — would force the database to group every row, completed or not, before throwing most of that work away. See Why HAVING Is Used After GROUP BY for exactly why the order is fixed this way.

Q3: GROUP BY multiple columns

group-by-multiple.sql
SELECT region, status, COUNT(*)
FROM orders
GROUP BY region, status;
Common guess (grouped by region only)
regioncount
West7
East5
Actual output
regionstatuscount
Westpending3
Westcompleted4
Eastcompleted5

The result has one row per distinct (region, status) pair, not one row per region — West appears twice because it has two different status values, each forming its own group.

Q4: Find duplicate rows with GROUP BY

find-duplicates.sql
SELECT email, COUNT(*) AS occurrences
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;

This is the standard duplicate-finding pattern: group by the column that defines a "duplicate," then keep only the groups with more than one row. It's also a natural companion to the self-join approach in SQL Tricky Interview Questions With Answers, which returns the actual duplicate row pairs rather than just the count.

Q5: What does ROLLUP add?

rollup-example.sql
SELECT region, status, COUNT(*)
FROM orders
GROUP BY ROLLUP(region, status);
Plain GROUP BY (per-group rows only)
regionstatuscount
Westpending3
Westcompleted4
ROLLUP (adds subtotal + grand total)
regionstatuscount
Westpending3
Westcompleted4
WestNULL7
NULLNULL7

ROLLUP layers extra summary rows on top of the normal grouped output — a subtotal per region (status = NULL marks the subtotal row) and a final grand total row, all in one query instead of a manual UNION of several separately-aggregated queries. More patterns like this in Advanced GROUP BY Techniques You Should Know.

Q6: GROUP BY vs DISTINCT

Without any aggregate function, GROUP BY column and SELECT DISTINCT column return identical results — both simply collapse rows down to one per distinct value. They diverge the moment an aggregate is needed, since only GROUP BY can pair the grouped column with a per-group computed value like COUNT(). Full comparison in SQL GROUP BY Without Aggregate Function.

Key takeaways

  • Every SELECT column must be grouped or aggregated — a naive guess of "picks any row" is wrong; it's a hard error.
  • WHERE filters rows before grouping; HAVING filters groups after — order is fixed, not interchangeable.
  • GROUP BY on multiple columns groups by their combination, producing one row per distinct pair.
  • HAVING COUNT(*) > 1 after grouping by the duplicate-defining column is the standard duplicate finder.
  • ROLLUP adds subtotal and grand-total rows to a normal GROUP BY result in a single query.