skip to content

Why does SELECT MAX(COUNT(*)) FROM orders GROUP BY customer_id fail, and how do you rewrite it?

level: middleimportance: should knowfreq 52%

answer

  1. Count how many levels of grouping are declared
  2. The inner function already collapsed the rows
  3. One GROUP BY cannot serve two aggregates
  4. The fix adds a query level, not a clause
  5. ROUND(AVG(x),2) is not the same shape

basics

~20 s

Standard SQL forbids one aggregate taking another as its argument at the same query level: COUNT already collapses each group to a single value, leaving nothing for MAX to aggregate. Compute the counts in a subquery, then aggregate that result.

solid answer

~50 s

An aggregate consumes a set of rows and yields one value per group. Once `COUNT(*)` has run, each customer is already a single row, so there is no second set for `MAX` to reduce — the query names two levels of aggregation but declares only one `GROUP BY`. Standard SQL therefore rejects an aggregate whose argument contains another aggregate at the same query level; engines report it as something like *aggregate function calls cannot be nested*. The portable fix is to make the second level explicit with a derived table or CTE: group once inside, aggregate the result outside. ```sql SELECT MAX(order_count) AS busiest FROM (SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id) AS per_customer; ``` Note the restriction is about *aggregates*, not about function calls generally — `ROUND(AVG(price), 2)` is perfectly legal.

code

sql · 8 lines
sql
-- rejected: two levels of aggregation, one GROUP BY
SELECT MAX(COUNT(*)) FROM orders GROUP BY customer_id;

-- portable rewrite: make the second level a real query level
SELECT MAX(order_count) AS busiest
FROM (SELECT customer_id, COUNT(*) AS order_count
      FROM orders
      GROUP BY customer_id) AS per_customer;

go deeper

for a junior

Recognise that an aggregate cannot take another aggregate as its argument, and be able to write the derived-table rewrite with an alias on the subquery.

for a middle

Explain why, in pipeline terms: the inner aggregate already collapsed each group, so the outer one has no set left to reduce and the query declares only one grouping level. Distinguish it from legal shapes like ROUND(AVG(x),2).

for a senior

Generalise the fix — a second level of aggregation means a second query level — and show the CTE form plus the ORDER BY with a row limit when the question is really 'which one', including how ties behave.

for a principal

Use it to make a readability argument: multi-level aggregation expressed as named CTE steps is auditable and portable, whereas relying on a dialect that tolerates nesting quietly ties reporting SQL to one engine.

## The error and what it really means This looks reasonable and is rejected: ```sql SELECT MAX(COUNT(*)) FROM orders GROUP BY customer_id; ``` The intent is clear — "how many orders does the busiest customer have?" — but the statement asks for **two levels of aggregation while declaring one**. Work through the pipeline. `GROUP BY customer_id` partitions the rows into one group per customer. `COUNT(*)` reduces each group to a single number. At that point the query has produced one row per customer, and grouping is finished. `MAX` needs a *set* of values to reduce, but no clause in this query says which set that is or how it should be grouped, so the statement has no meaning to give. Standard SQL states the rule directly: an aggregate function's argument may not contain another aggregate function evaluated at the same query level. Engines surface it with messages such as *aggregate function calls cannot be nested* or *cannot use an aggregate or a subquery in an expression used for the group by list*. ## The portable rewrite Make the second level a real query level. A derived table is the canonical form and works everywhere: ```sql SELECT MAX(order_count) AS busiest FROM (SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id) AS per_customer; ``` The alias on the derived table is not decoration — several engines require every derived table to be named. A `WITH` clause expresses the same thing and usually reads better when there are two or three such steps: ```sql WITH per_customer AS ( SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id ) SELECT MAX(order_count) FROM per_customer; ``` If you also want to know *which* customer, do not reach back for a nested aggregate — order the grouped result and take the top row: ```sql SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id ORDER BY order_count DESC FETCH FIRST 1 ROW ONLY; ``` That returns one customer even when several tie; if ties must all appear, you need an explicit tie-handling construct rather than a row limit. ## What the rule does *not* forbid Three neighbouring shapes are legal, and mixing them up is the usual source of confusion. **Ordinary functions wrapping an aggregate are fine.** `ROUND(AVG(price), 2)`, `COALESCE(SUM(amount), 0)` and `CAST(COUNT(*) AS DECIMAL(10,2))` all take a single already-computed value as input. The prohibition is specifically aggregate-inside-aggregate. **Arithmetic between aggregates is fine.** `SUM(revenue) / COUNT(*)` and `MAX(price) - MIN(price)` combine two values that were each computed over the same group — one level, two aggregates, no nesting. **Aggregates inside an aggregate's argument expression are fine when only the outer one is an aggregate.** `SUM(price * quantity)` aggregates an expression over rows; nothing is nested. ## A dialect caveat worth mentioning At least one major engine relaxes this. Oracle permits a single level of nesting when a `GROUP BY` is present, so `SELECT MAX(COUNT(*)) FROM orders GROUP BY customer_id` runs there and returns the largest per-customer count. Treat that as a portability note, not a technique: the derived-table form runs everywhere, expresses the two levels explicitly, and extends naturally when someone later asks for the second-busiest customer or for the average of the counts. If you name the exception in an interview, say the rest of the sentence too — "…and I would still write the subquery". ## Why interviewers like this question It is a compact test of whether you understand that aggregation is a *level*, not just a function call. A candidate who says "you can't nest aggregates" has memorised a rule; a candidate who says "COUNT already collapsed each group, so MAX has nothing left to consume — I need a second query level" has the model. The second answer also predicts, without being told, that the same fix applies to `AVG(COUNT(*))`, `SUM(MAX(x))` and every other pair.

  • Is ROUND(AVG(price), 2) also forbidden by the no-nesting rule?
    No. `ROUND` is an ordinary scalar function taking one already-computed value, so there is only one level of aggregation. The prohibition is specifically an aggregate inside an aggregate's argument at the same query level. Arithmetic between aggregates, such as `MAX(price) - MIN(price)`, is legal for the same reason.
  • Is MAX(COUNT(*)) ever accepted by a real engine?
    Oracle permits a single level of nesting when a `GROUP BY` is present, and returns the largest per-customer count for that query. It is a dialect extension, not standard behaviour, and other majors reject it. Write the derived-table form anyway: it is portable, states the two levels explicitly, and survives the follow-up asking which customer it was.
  • How would you also return which customer has the largest count?
    Group once, then order and limit: `SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id ORDER BY order_count DESC FETCH FIRST 1 ROW ONLY`. That yields one customer even on a tie; if every tied customer must appear, you need explicit tie handling rather than a plain row limit.

saying these in an interview costs you the question

  • Saying it fails because GROUP BY is in the wrong place
  • Trying to fix it by adding HAVING
  • Claiming no function may ever wrap an aggregate
  • Believing a second GROUP BY column would make it legal
  • Treating Oracle's extension as standard SQL

context