What does a HAVING clause do in a query that has no GROUP BY?
answer
- The clause is legal even with nothing to group
- Ask what the group is when none is declared
- One group means one test
- Compare row counts with and without the clause
basics
~10 sWithout GROUP BY, every row surviving WHERE forms one implicit group, and HAVING is a single test over that group's aggregates. The query returns either the one aggregate row or no rows at all.
solid answer
~40 s`HAVING` does not require `GROUP BY`. When there is no `GROUP BY`, the whole set of rows that passed `WHERE` is treated as a single implicit group, and `HAVING` is one boolean test over that group's aggregate values: ```sql SELECT COUNT(*) AS failed_logins FROM audit_log WHERE event = 'LOGIN_FAILED' HAVING COUNT(*) > 100; ``` This returns one row if there were more than 100 failures, and **zero rows** otherwise — which is the interesting part, because an aggregate query without `HAVING` always returns exactly one row, even over an empty table. So `HAVING` here converts "always one row, possibly showing 0" into "a row only when the condition holds". The same restriction as always applies: with no grouping columns, the SELECT list and the `HAVING` predicate may reference only aggregates and constants.
code
sql · 10 lines-- always exactly one row, possibly showing 0
SELECT COUNT(*) AS failed_logins
FROM audit_log
WHERE event = 'LOGIN_FAILED';
-- one row only when the threshold is crossed, otherwise no rows
SELECT COUNT(*) AS failed_logins
FROM audit_log
WHERE event = 'LOGIN_FAILED'
HAVING COUNT(*) > 100;go deeper
Know that HAVING is legal without GROUP BY and that the whole table then counts as one group. Recognising the form when you read it is enough at this level.
Explain the implicit single group and the row-count consequence: with HAVING the query may return zero rows, whereas the same aggregate query without it always returns exactly one.
Point out the effect on calling code and on generated SQL — a threshold query that returns no rows instead of a 0 will read as 'no data' downstream unless the client is written for it.
Treat the empty-versus-zero distinction as an interface decision: agree on whether aggregate endpoints return a row with 0 or nothing at all, so dashboards and alert rules across teams interpret silence the same way.
## The implicit single group An aggregate query with no `GROUP BY` is not un-grouped; it is grouped into exactly one group containing every row that survived `WHERE`. That is why `SELECT COUNT(*) FROM orders` returns one row rather than one row per order. Since `HAVING`'s job is to test groups, and this query has a group, `HAVING` is perfectly legal without `GROUP BY`. ```sql SELECT COUNT(*) AS failed_logins FROM audit_log WHERE event = 'LOGIN_FAILED' AND occurred_at >= DATE '2026-08-01' HAVING COUNT(*) > 100; ``` `WHERE` narrows the rows; the survivors form one group; `COUNT(*)` counts them; `HAVING` tests that single count once. ## The behaviour that surprises people An aggregate query without `HAVING` returns **exactly one row, always** — including over an empty input, where `COUNT(*)` is 0 and `SUM(x)` is NULL. Adding `HAVING` breaks that guarantee on purpose: if the predicate is not TRUE, the single group is discarded and the result is an empty set. ```sql -- always one row: either a number or 0 SELECT COUNT(*) FROM audit_log WHERE event = 'LOGIN_FAILED'; -- one row, or none at all SELECT COUNT(*) FROM audit_log WHERE event = 'LOGIN_FAILED' HAVING COUNT(*) > 100; ``` That distinction matters to calling code. A client that does `if (resultSet.next())` behaves very differently between the two forms, and a threshold check written as the second form silently returns "no rows" rather than "0" when nothing matched. ## What may appear in the query With no `GROUP BY` there are no grouping columns, so there is nothing that could be referenced as a plain column: the SELECT list and the `HAVING` predicate may contain only aggregates, literals, expressions over those, and correlated references from an outer query. `SELECT customer_id, COUNT(*) FROM orders HAVING COUNT(*) > 5` is invalid — `customer_id` has many values inside the single group. As elsewhere, `HAVING` may test an aggregate that is not selected: ```sql SELECT AVG(amount) AS avg_amount FROM orders WHERE order_date >= DATE '2026-01-01' HAVING COUNT(*) >= 30; -- only report the average if the sample is big enough ``` That reads as a guard: publish the average only when at least 30 observations back it. ## The same predicate is not a WHERE predicate Beginners sometimes try to compress this into `WHERE COUNT(*) > 100`, which is rejected: `WHERE` runs before the implicit group exists. The presence or absence of `GROUP BY` changes nothing about that — an aggregate is never legal in `WHERE`. ## Where you actually meet it The form shows up in threshold and alerting queries ("emit a row only if the error count crossed the limit"), in existence-style probes over aggregates, and inside subqueries where returning zero rows instead of a zero is exactly what an `EXISTS` or `NOT EXISTS` test wants. It also appears in generated SQL, where a reporting layer appends a `HAVING` filter without knowing whether the query grouped anything. ## Style note Because the shape is uncommon, spell out the intent when you use it: a comment such as `-- returns no rows unless the threshold is crossed` saves the next reader from assuming a missing `GROUP BY`. When you want a value rather than a presence signal, drop the `HAVING` and let the single row report the number, or wrap the query so the caller always gets a row.
- How many rows can SELECT COUNT(*) FROM orders HAVING COUNT(*) > 5 return?Zero or one. The whole table forms a single implicit group, so there is exactly one candidate row; HAVING either keeps it or removes it. Without the HAVING clause the same query always returns exactly one row, showing 0 for an empty table — that is the practical difference between the two forms and it matters to whatever consumes the result.
- Why can't you add a plain column such as customer_id to a HAVING query that has no GROUP BY?There is one group covering every row, so customer_id has as many values as there are rows and no single value to return or compare. Only aggregates, literals and expressions over them are legal. To report per-customer values you need GROUP BY customer_id, which creates one group per customer and makes the column a grouping key.
saying these in an interview costs you the question
- Says HAVING is a syntax error without GROUP BY
- Thinks the query still always returns one row
- Expects a 0 to be returned when the threshold is not met
- Tries to write the same threshold as WHERE COUNT(*) > 100
- Adds a non-aggregate column to the select list