SQL Analysis · Chapter 3 of 10

GROUP BY and HAVING

Aggregate PostgreSQL data into trustworthy metrics with grouped calculations and post-aggregate filters.

Why this chapter matters

Team decisions often depend on rollups such as weekly totals, conversion by segment, and top categories.

What you will learn

  • Group rows correctly and compute aggregates like COUNT, SUM, and AVG.
  • Use HAVING to filter aggregated groups after calculation.
  • Avoid mixing non-grouped columns with aggregates incorrectly.

Understand the core ideas

Aggregation turns detailed rows into summarized metrics, so correctness depends on grouping dimensions and measure definitions. In PostgreSQL, every selected column must either be grouped or aggregated, which forces you to make dimension choices explicit. That is helpful because many reporting bugs come from accidental dimensions, like grouping by plan and month when you only wanted month. Start by writing the smallest valid grouped query: dimensions in GROUP BY, one measure such as SUM(amount), and no optional fields. Then add additional measures one by one and validate each against known totals. For counts, decide whether you need raw row count, distinct entity count, or conditional count. COUNT(*) answers different business questions than COUNT(DISTINCT user_id). Analysts who state this choice in plain language before coding avoid many downstream disputes about why numbers differ between dashboards.

WHERE and HAVING operate at different stages in PostgreSQL query processing. WHERE filters base rows before grouping, while HAVING filters grouped rows after aggregates are computed. Use WHERE for row level constraints like date range or paid status, and HAVING for aggregate thresholds like revenue above 5000. Mixing these up either throws errors or silently changes metric meaning. A robust QA approach is to test a query on a single month and one plan where you can manually verify results. You can also compare the grouped output sum back to a trusted base query to ensure no rows were dropped unexpectedly. Keep row grain visible in your output labels, for example one row per month_start, plan_id. When teams document both grain and measure formula, reports become easier to maintain and audits become much faster.

Key terms

dimension
A grouped column that defines how rows are partitioned in summary output.
measure
A computed aggregate value such as SUM(revenue) or COUNT(*).
pre-aggregate filter
A WHERE condition applied before GROUP BY and aggregate calculation.
post-aggregate filter
A HAVING condition evaluated after grouped metrics are computed.

Monthly revenue by plan with threshold filtering

Use invoices(invoice_id, plan_id, amount, state, paid_at) at one row per invoice. Goal output grain is one row per month and plan for paid invoices only.

  1. Filter to paid invoices and a known time range in WHERE so only qualifying base rows enter aggregation.
  2. Create a month bucket using date_trunc('month', paid_at)::date and include plan_id as the second grouping dimension.
  3. Compute SUM(amount) as monthly_revenue and verify that each output row now represents a unique (month_start, plan_id) pair.
  4. Apply HAVING SUM(amount) > 5000 to keep only plan-month groups that exceed the business threshold.
Result: SELECT date_trunc('month', paid_at)::date AS month_start, plan_id, SUM(amount) AS monthly_revenue FROM invoices WHERE state = 'paid' AND paid_at >= DATE '2026-01-01' AND paid_at < DATE '2027-01-01' GROUP BY date_trunc('month', paid_at)::date, plan_id HAVING SUM(amount) > 5000 ORDER BY month_start, plan_id; returns only high-revenue plan-month groups with explicit summary grain.

A common misconception

Claim: HAVING is just another way to write WHERE, so they are interchangeable.

Correction: They are not interchangeable. WHERE filters raw rows before grouping, while HAVING filters grouped results after aggregates are computed. Choosing the wrong one changes the metric.

Lessons in this chapter

  1. Aggregation fundamentalsBuild grouped metrics with clear dimension columns and measure columns. Read the full guide →
  2. HAVING versus WHEREApply WHERE before grouping and HAVING after grouping in PostgreSQL query order.
  3. Distinct and conditional countsUse COUNT(DISTINCT ...) and CASE-based aggregates for cleaner KPI definitions.
  4. Metric QA checksValidate grouped totals against known baselines to catch logic mistakes early.

Study task

In PostgreSQL, compute monthly revenue by plan and return only plans whose monthly revenue exceeds 5000.

Chapter checkpoint

When filtering groups with total orders greater than 100, should you use WHERE or HAVING?

Use HAVING because the filter depends on an aggregate computed after GROUP BY.

Learn this with an AI teacher that starts from what you already know.

Tell LearnLive your goal and starting point, and it adapts the explanations, examples, and practice as you go.

Teach me this