October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk5 min

GROUP BY and Aggregate Functions Explained: The WHERE vs HAVING Mistake Almost Everyone Makes

GROUP BY builds groups, aggregates summarize them, WHERE filters rows before grouping, and HAVING filters groups after aggregation. Here is how to use each one correctly.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

GROUP BY collapses rows that share a value into one group, and aggregate functions such as COUNT, SUM, and AVG then calculate a summary for each group. The most common mistake in this area is putting a condition on an aggregate result into WHERE. WHERE filters individual rows before any grouping happens, so it cannot test a count or an average. That condition belongs in HAVING, which filters groups after the aggregates have been calculated.

What GROUP BY does

A table stores one row per record. Many questions, however, are about sets of rows: how many employees each department has, or total sales per region. GROUP BY answers these by gathering input rows that share the same value in one or more grouping expressions. Each distinct value becomes one group, and the query returns one output row per group.

As an Amazon Associate I earn from qualifying purchases.

Without GROUP BY, a query that uses an aggregate treats all of the selected rows as a single group. That is why SELECT COUNT(*) FROM orders; returns one number for the whole table.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Aggregate functions

An aggregate function takes many input values and returns one value. The five you will meet first are shown below. Behavior described here follows the SQL standard and the major engines; verify dialect-specific details in your database’s documentation before relying on them.

Function Returns Typical use NULL handling
COUNT(*) Number of rows in the group How many employees per department Counts every row, including rows where columns are NULL
COUNT(column) Number of non-NULL values in the column How many employees have a manager assigned Skips NULL values
SUM(column) Total of the values Total payroll per department Skips NULL values
AVG(column) Arithmetic mean of the non-NULL values Average salary per department Skips NULL values, so it is not the same as dividing SUM by COUNT(*)
MIN(column) / MAX(column) Smallest or largest value Earliest hire date, highest order total Skips NULL values

The NULL column matters more than it looks. If some salaries are missing, AVG(salary) averages only the rows that have a salary, while COUNT(*) still counts those employees. Decide which of these you actually want before you read a result as a headcount or a pay figure.

Reading a complete GROUP BY query

Consider this query:

SELECT department,
       COUNT(*)    AS employee_count,
       AVG(salary) AS average_salary
FROM employees
WHERE active = TRUE
GROUP BY department
HAVING COUNT(*) >= 5;

Each clause does one job:

  1. FROM employees names the input rows.
  2. WHERE active = TRUE removes individual rows that are not active. Those rows never enter a group.
  3. GROUP BY department creates one group per department among the remaining rows.
  4. COUNT(*) and AVG(salary) calculate a value for each group from only the rows that survived the WHERE step.
  5. HAVING COUNT(*) >= 5 discards any group whose count is below five. The aggregate is available here because it has already been computed.
  6. SELECT produces the output columns for the groups that remain.

The query therefore answers: among active employees, which departments have at least five people, and what is each one’s average salary?

The WHERE vs HAVING mistake

The mistake usually appears when someone wants to filter on a count or average and writes it in WHERE:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
SELECT department, COUNT(*) AS employee_count
FROM employees
WHERE COUNT(*) >= 5
GROUP BY department;

This fails. At the point where WHERE runs, no groups exist yet, so there is no count to test. PostgreSQL rejects the statement with the error aggregate functions are not allowed in WHERE, and SQL Server and other engines refuse it in a similar way. The fix is to move the condition to HAVING, as in the example above.

The reverse mistake is less likely to produce an error but still matters. A condition on a row-level value can be placed in HAVING when it references a grouped column, such as HAVING department <> 'HR'. The result is the same as putting it in WHERE, because a group is either kept or dropped as a whole. The difference is cost and meaning: the engine computes aggregates for every row first, then throws away whole groups. Put row-level conditions in WHERE so excluded rows never reach the aggregate calculation.

Which clause to use

Condition is about Example Clause
An individual source row active = TRUE, hire_date >= '2020-01-01' WHERE
An aggregate value COUNT(*) >= 5, AVG(salary) > 80000 HAVING
A grouped column, applied to whole groups department <> 'HR' WHERE preferred; HAVING gives the same rows but does more work

The order of operations

For learning purposes, the logical sequence is:

  • FROM builds the input rows.
  • WHERE keeps only the rows that satisfy row-level conditions.
  • GROUP BY forms groups, and aggregates are calculated for each one.
  • HAVING keeps only the groups that satisfy group-level conditions.
  • SELECT builds the output columns, and ORDER BY sorts the result.

This is the model PostgreSQL’s SELECT documentation describes. It explains what the result should be, not how the engine executes the query internally. Optimizers may reorder work as long as the result is the same.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Rules for selected columns

Every nonaggregated column in the SELECT list must be handled by the grouping. In the example above, department is in both SELECT and GROUP BY, so each output row has one department value. Selecting a column such as first_name without grouping it or wrapping it in an aggregate is invalid in strict SQL. Microsoft’s SQL Server documentation requires each nonaggregate column in the SELECT list to appear in GROUP BY.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

MySQL has historically been more permissive, and its 8.4 Reference Manual describes the ONLY_FULL_GROUP_BY setting that enforces the strict rule. MySQL also allows some references to select-list aliases and expressions inside GROUP BY and HAVING. Those conveniences are not part of standard SQL and are not portable. Other engines, including PostgreSQL and SQL Server, do not accept select-list aliases in WHERE or HAVING, so write the full expression there.

Troubleshooting a grouped query

  • An aggregate error appears in WHERE. Move the condition to HAVING, or wrap the query in a subquery if you need the aggregate in a row-level filter.
  • A column is rejected as not grouped. Add it to GROUP BY, or apply an aggregate such as MIN or MAX if one value per group is what you want.
  • Averages look too low or too high. Check for NULL values. AVG skips them, and COUNT(*) does not.
  • A group count is unexpected. Check whether a WHERE condition ran before the grouping. Rows excluded there never contribute to any count.
  • The same query works in one database and fails in another. Compare how each engine handles nonaggregate columns and select-list aliases in GROUP BY and HAVING.

The core rule holds across databases: conditions on source rows go in WHERE, and conditions on aggregate results go in HAVING.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.