Data AnalyticsSQL for Analyticsbeginner
Updated:

GROUP BY and HAVING for Reporting

4 min read

Break totals down by region, month or product, then keep only the groups that matter. Learn GROUP BY and HAVING as analysts use them for segmented reports.

TL;DR – Quick Answer

GROUP BY splits rows into groups that share a value — like region or month — and runs aggregate functions on each group separately, turning one table into a segmented report. HAVING then filters those groups using aggregate conditions, such as keeping only regions with revenue above a threshold. The rule to remember: WHERE filters rows before grouping, HAVING filters groups after, and every selected column must be a grouping key or an aggregate.

On This Page

A top-line total answers "how much did we sell?" but the interesting business questions are always segmented: how much per region, per month, per product, per customer segment. GROUP BY is the clause that produces those breakdowns, and HAVING is how you keep only the segments worth reporting. Together they turn a flat table of orders into the kind of dimensional report that fills a dashboard.

This builds directly on aggregate functions — make sure SUM, COUNT and AVG feel natural before continuing. It is a central stop on the SQL for analytics path.

From one total to a breakdown

Here is the sample orders table:

-- orders
order_id | customer | region | product  | amount | order_date
---------+----------+--------+----------+--------+-----------
1001     | Aarti    | South  | Keyboard | 1200   | 2026-01-05
1002     | Bhaskar  | South  | Monitor  | 8500   | 2026-01-11
1003     | Chitra   | West   | Keyboard | 1200   | 2026-02-02
1004     | Aarti    | South  | Mouse    | 600    | 2026-02-18
1005     | Devan    | North  | Monitor  | 8500   | 2026-03-09
1006     | Chitra   | West   | Headset  | 2100   | 2026-03-22

SUM(amount) alone gives one number for the whole table. Add GROUP BY region and the database splits the rows into per-region groups, then sums each group:

SELECT region,
       COUNT(*)    AS orders,
       SUM(amount) AS revenue
FROM orders
GROUP BY region;
region | orders | revenue
-------+--------+--------
South  | 3      | 10300
West   | 2      | 3300
North  | 1      | 8500

One row per region, each with its own order count and revenue. Swap region for product and you get a per-product breakdown from the same pattern. This is the fundamental move of segmented reporting.

The rule that causes most GROUP BY errors

Memorize this: every column in the SELECT list must either appear in GROUP BY or be inside an aggregate function. Break it and the query fails.

-- ERROR: customer is neither grouped nor aggregated
SELECT region, customer, SUM(amount)
FROM orders
GROUP BY region;

Once rows collapse into the South group there are three different customers in it, so the database cannot pick one value for customer. Either add it to the grouping key or aggregate it away. The mental model that fixes this permanently: after GROUP BY, each group is one output row, so any column you display must have exactly one value per group.

Grouping by multiple dimensions

GROUP BY region, product makes one group per unique combination, producing a cross-tab:

SELECT region, product,
       SUM(amount) AS revenue
FROM orders
GROUP BY region, product
ORDER BY region, revenue DESC;

This is how you build reports like "revenue by region and product category" — two dimensions at once. The more columns you group by, the finer and more numerous the groups.

Grouping by month

A constant analyst need is time-series summaries. Group by a truncated date so every order in a month lands in one group:

SELECT DATE_TRUNC('month', order_date) AS month,
       SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;
month      | revenue
-----------+--------
2026-01-01 | 9700
2026-02-01 | 1800
2026-03-01 | 10600

The exact date function varies by database — DATE_TRUNC in PostgreSQL, DATE_FORMAT in MySQL, FORMAT in SQL Server — but the idea is identical: collapse the timestamp to the granularity you want, then aggregate.

Filtering groups with HAVING

WHERE filters rows before grouping. But "only regions with revenue above 5000" is a condition on a group total that does not exist until after grouping. That is what HAVING is for:

SELECT region,
       SUM(amount) AS revenue
FROM orders
GROUP BY region
HAVING SUM(amount) > 5000;
region | revenue
-------+--------
South  | 10300
North  | 8500

West drops out because its 3300 is below the threshold. You often use both clauses together — WHERE to discard irrelevant rows first, then HAVING to keep only qualifying groups:

SELECT region,
       SUM(amount) AS revenue
FROM orders
WHERE order_date >= '2026-01-01'   -- filter rows
GROUP BY region
HAVING SUM(amount) > 5000          -- filter groups
ORDER BY revenue DESC;
Clause Filters Runs Can use aggregates?
WHERE Individual rows Before grouping No
HAVING Whole groups After grouping Yes

Common mistakes

  • Using WHERE to filter an aggregate. WHERE SUM(amount) > 5000 fails; the count or sum does not exist yet. Use HAVING.
  • Selecting an ungrouped, unaggregated column. The single most common GROUP BY error. Every displayed column must be a grouping key or an aggregate.
  • Grouping by a raw timestamp for a monthly report. If each order has a distinct time, grouping by the raw column gives one group per order. Truncate the date to month first.
  • Double-counting after a join. Joining before grouping can multiply rows and inflate SUM. Verify totals against a known figure when a join precedes the grouping.

In interviews

GROUP BY questions are the core of analyst SQL screens: "revenue per region", "orders per customer", "monthly sales trend", "regions above a revenue target". The near-guaranteed conceptual question is "what is the difference between WHERE and HAVING?" — answer that WHERE filters rows before aggregation and cannot use aggregates, HAVING filters groups after and can, and mention the execution order to show depth. Interviewers also probe the ungrouped-column rule, so be ready to explain why it exists.

Where this fits in your learning path

GROUP BY and HAVING turn the aggregate functions into segmented reports, the backbone of dashboards. Once you want names instead of IDs in those reports, you combine grouping with joins. And when you need per-row detail alongside a group summary rather than collapsing it, move to window functions. All three recur throughout the data analyst roadmap.

Frequently Asked Questions

What is the difference between WHERE and HAVING?
WHERE filters individual rows before grouping and cannot use aggregates. HAVING filters whole groups after aggregation and can reference SUM, COUNT or AVG. In a sales report, WHERE might keep only completed orders, and HAVING might keep only regions whose total revenue exceeds a target.
How do I group sales by month in SQL?
Extract the month from the date and group by it, for example GROUP BY DATE_TRUNC('month', order_date) in PostgreSQL or FORMAT/MONTH functions elsewhere. Group by the truncated date rather than the raw timestamp so all orders in a month collapse into one group. Then aggregate revenue or order counts per month.
Why must every non-aggregated column be in GROUP BY?
Because after grouping, each group becomes one output row, so any column shown must have exactly one value per group. A column that is neither a grouping key nor aggregated could have many values in a group, and the database cannot choose one. Add it to GROUP BY or wrap it in an aggregate.
Can I use both WHERE and HAVING in the same query?
Yes, and analysts often do. WHERE narrows the rows before grouping, then HAVING filters the resulting groups. For example, keep only 2026 orders with WHERE, group by region, then keep only regions above a revenue target with HAVING. They operate at different stages.
Can I group by more than one column?
Yes. GROUP BY region, product creates one group per unique combination of region and product, producing a cross-tabulated report. Aggregates then compute for each combination. Adding more grouping columns makes groups finer and more numerous.

Want to Build Your Career in Data Analytics with AI?

Join CodeBegun and train with working industry engineers — See the Data Analytics course in Hyderabad

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 16 July 2026 LinkedIn
Chat with us