skip to content

Aggregation and Grouping

GROUP BY mechanics and the aggregate functions, how aggregates treat NULLs, HAVING versus WHERE, GROUPING SETS, ROLLUP and CUBE, conditional and string aggregation, and the fan-out you get aggregating over a join. Interviewers ask because a doubled SUM caused by a one-to-many join is the classic reporting bug.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

COUNT, SUM, AVG, MIN and MAX: what argument types does each accept and return?

level: juniorimportance: must knowfreq 78%

answer

  1. Three families, not five separate rules
  2. One needs no argument at all
  3. Two demand numbers, two demand order
  4. MAX(hire_date) keeps the DATE type
  5. Result type need not match argument type

basics

~20 s

COUNT accepts any expression, or *, and returns an integer count. SUM and AVG require numeric arguments and return numeric results. MIN and MAX accept any orderable type — numbers, text, dates — and return a value of that same type.

solid answer

~40 s

The five core aggregates split into three families by argument type. **COUNT** is the most permissive: `COUNT(*)` takes no column at all, and `COUNT(expr)` accepts an expression of any type, because it only asks "is this present?". It returns an exact integer. **SUM** and **AVG** are arithmetic, so they demand a numeric argument — `SUM(last_name)` is a type error, not a zero. **MIN** and **MAX** need only an *orderable* type, so they work on numbers, character strings, dates and timestamps, and they return a value of the argument's own declared type: `MAX(hire_date)` is a DATE. The result type is not always the argument type, though — SUM often widens (integer input, bigint or decimal output), and AVG's result type is implementation-defined, which is where integer-average surprises come from.

go deeper

for a junior

Memorise the three families: COUNT takes anything (or *), SUM and AVG take numbers, MIN and MAX take anything orderable. Be able to say what MAX(hire_date) returns without hesitating.

for a middle

Explain the result types, not just the argument types — why SUM often widens the integer it was given, and why the standard leaves AVG's precision implementation-defined. Name the DISTINCT quantifier and where it is meaningful.

for a senior

Show you have been bitten: point out that the two classic production bugs, truncated integer averages and overflowing integer sums, both fall out of result-type rules, and say how you cast to avoid them.

for a principal

Frame it as a portability contract: the argument types are standard, the result types are partly implementation-defined, so a team shipping SQL across engines should pin the numeric type at the source rather than trusting each engine's widening rules.

## What an aggregate function is An aggregate function collapses a set of input rows into a single value. Applied to a query with no `GROUP BY`, the whole result set is one group and the aggregate produces exactly one row; with a `GROUP BY`, it produces one value per group. Standard SQL defines five core aggregates — `COUNT`, `SUM`, `AVG`, `MIN`, `MAX` — and essentially every aggregation exercise in an interview is composed from them, so their signatures are vocabulary you need cold. ## COUNT: any type in, an integer out `COUNT` is the only aggregate with a special `*` form. `COUNT(*)` takes no argument and counts rows. `COUNT(expr)` takes an expression of **any** type — character, date, numeric, boolean — because it never inspects the value arithmetically, only whether one is present. Both return an exact numeric integer. The width of that integer is engine-specific: PostgreSQL's `count()` returns `bigint`, while SQL Server's `COUNT` returns `int` and offers `COUNT_BIG` when the count can exceed an `int`. That matters mainly when you divide by a count and land in integer arithmetic. ```sql SELECT COUNT(*), COUNT(email), COUNT(hire_date) FROM employees; ``` ## SUM and AVG: numbers only `SUM` and `AVG` are arithmetic, so the standard requires a numeric argument. `SUM(last_name)` is a *type error* — a conforming engine rejects the statement rather than returning zero. (Some engines apply loose implicit conversion instead; that is an engine quirk, not something to rely on.) Several engines also extend the pair to interval types, where summing durations is meaningful. Their result types follow rules worth knowing: - `SUM` of an exact numeric (INTEGER, DECIMAL) is exact numeric; `SUM` of an approximate numeric (REAL, DOUBLE PRECISION) is approximate. Engines usually *widen* the exact case — PostgreSQL's `sum(integer)` returns `bigint` — precisely so that adding many values does not overflow. Engines that do **not** widen can raise an arithmetic overflow. - `AVG` returns an exact numeric for exact input, but the standard leaves the precision and scale implementation-defined. That is exactly why averaging an INTEGER column returns a whole number on some engines and a fractional value on others. ## MIN and MAX: anything you can order `MIN` and `MAX` require only that the argument type has a defined ordering. That covers numeric types, character strings, `DATE`, `TIME`, `TIMESTAMP`, and typically anything else the engine can sort. They return a value of the argument's **own declared type**: `MAX(hire_date)` is a `DATE`, `MIN(city)` is a string of the column's type. What "largest" means is decided by that type's ordering rules — chronological for dates, numeric for numbers, and the column's *collation* for character data. That is why `MAX(version_label)` over the text values `'9'`, `'10'`, `'11'` returns `'9'`: string ordering compares character by character. ```sql SELECT MIN(hire_date), MAX(hire_date), MIN(city), MAX(salary) FROM employees; ``` ## Rules the five share Three properties apply across the family: 1. **Every aggregate returns exactly one value per group** — including when the group is empty of usable values, in which case you get one row, not zero. 2. **All of them except `COUNT(*)` ignore NULL inputs**, since NULL means "unknown" rather than a value to add or compare. 3. **The standard allows a `DISTINCT` quantifier inside `COUNT`, `SUM` and `AVG`** — `SUM(DISTINCT amount)` sums each distinct value once. `MIN(DISTINCT x)` is accepted but pointless, since removing duplicates cannot change an extreme. One restriction also applies to all of them: an aggregate may not take another aggregate as its argument at the same query level. `MAX(COUNT(*))` is rejected; you compute the counts in a subquery and aggregate that. ## Interview framing Interviewers use this as a screening question because a candidate who cannot say "`SUM` needs a number, `MIN`/`MAX` need an order, `COUNT` needs nothing" will fumble every later exercise. Say the three families, then add the sharp edge: the *result* type is not always the argument type, and both of the well-known traps — integer averages truncating and integer sums overflowing — come from those result-type rules rather than from the data.

  • Which of the five aggregates accept a DISTINCT quantifier, and which one makes no difference?
    Standard SQL allows `DISTINCT` inside `COUNT`, `SUM` and `AVG`, where it removes duplicate argument values before aggregating. `MIN(DISTINCT x)` and `MAX(DISTINCT x)` are syntactically accepted but semantically no-ops: dropping duplicates cannot change the smallest or largest value in a set.
  • What data type does COUNT return, and when does that width matter?
    An exact numeric integer, but the width is engine-specific — PostgreSQL returns `bigint`, while SQL Server's `COUNT` returns `int` and provides `COUNT_BIG` for counts beyond an `int`. It matters for very large tables and, more often, when you divide by a count and land in integer arithmetic instead of fractional division.
  • Is SUM(last_name) an error or does it just return zero?
    On a conforming engine it is a type error at parse or bind time: `SUM` is defined over numeric arguments only, so the statement never runs. Some engines apply permissive implicit conversion and quietly coerce the text, which is worse than failing because it can produce a silent wrong answer. Do not rely on it.

saying these in an interview costs you the question

  • Claiming SUM works on text and returns zero
  • Thinking MIN/MAX only apply to numbers
  • Assuming AVG always returns a fractional value
  • Saying COUNT can only take * or a numeric column
  • Believing the result type always equals the argument type

context

open as a page

Using SUM(CASE WHEN …), how do you count shipped and cancelled orders per customer?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Put a CASE inside the aggregate. SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END) contributes 1 for each matching row and 0 for the rest, so one GROUP BY query returns a separate count per condition.

open as a page

What do COUNT(*) and COUNT(city) return for a 10-row table where 3 rows have NULL city?

level: juniorimportance: must knowfreq 88%

basics

~10 s

COUNT() returns 10 and COUNT(city) returns 7. COUNT() counts rows; COUNT(expression) counts only the rows where that expression is not NULL, so the three NULL cities are skipped.

open as a page

What does GROUP BY do to a query's rows, and what determines the result row count?

level: juniorimportance: must knowfreq 85%

basics

~20 s

GROUP BY partitions the rows that survive FROM and WHERE into groups sharing the same grouping-key values, then emits exactly one row per group. The result has as many rows as there are distinct key combinations.

open as a page

What is the difference between the WHERE clause and the HAVING clause in SQL?

level: juniorimportance: must knowfreq 88%

basics

~20 s

WHERE filters individual rows before grouping; HAVING filters whole groups after aggregation. Only HAVING may reference aggregate results such as COUNT(*) or SUM(amount), because those values exist only once rows have been collapsed into groups.

open as a page

How would you use GROUP BY and HAVING to find duplicate email addresses in a users table?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Group by the column and keep only the groups with more than one row: SELECT email, COUNT() FROM users GROUP BY email HAVING COUNT() > 1. WHERE cannot express this, because the count exists only after grouping.

open as a page

What does SUM(o.amount) return when one order of 100 joins three order_items rows?

level: juniorimportance: must knowfreq 78%

basics

~10 s

300, not 100. The join repeats the order row once per matching item, so the same amount is added three times. That row multiplication is join fan-out, and it silently inflates SUM and COUNT.

open as a page

Why can AVG(salary) differ from SUM(salary) / COUNT(*) on the same table?

level: juniorimportance: must knowfreq 82%

basics

~10 s

AVG divides by the number of non-NULL salaries, because aggregates discard NULL inputs first. SUM(salary) / COUNT(*) divides by every row, NULL ones included. The two agree only when the column has no NULLs.

open as a page

Why does SELECT department, name, COUNT(*) FROM employees GROUP BY department fail?

level: juniorimportance: must knowfreq 80%

basics

~20 s

GROUP BY department collapses every department into one output row, and that row has many different name values, so name is ambiguous. Standard SQL requires each select-list column to be a grouping column or sit inside an aggregate.

open as a page

What does LISTAGG do, and what replaces it in engines that do not have it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

LISTAGG concatenates a group's values into one delimited string, as in LISTAGG(tag, ', ') WITHIN GROUP (ORDER BY tag). Engines lacking it spell the same idea STRING_AGG or GROUP_CONCAT, and ARRAY_AGG collects the values into an array instead.

open as a page

Why does AVG(score) return 4 rather than 4.5 when score is an INTEGER column?

level: middleimportance: must knowfreq 66%

basics

~20 s

AVG's result type is derived from its argument type, and the standard leaves the precision implementation-defined. Engines that keep the integer type for integer input discard the fractional part, so an average of 4 and 5 comes back as 4.

open as a page

How do SUM(CASE WHEN c THEN 1 ELSE 0 END) and COUNT(CASE WHEN c THEN 1 END) differ?

level: middleimportance: must knowfreq 62%

basics

~20 s

Both return the number of rows where the condition is true. COUNT relies on the missing ELSE producing NULL for non-matching rows, which COUNT ignores. Adding ELSE 0 to the COUNT form breaks it: 0 is not NULL, so it counts every row.

open as a page

After a LEFT JOIN, why does COUNT(*) return 1 for customers with no orders?

level: middleimportance: must knowfreq 75%

basics

~20 s

A LEFT JOIN keeps one manufactured row per unmatched customer, with every orders column NULL, and COUNT(*) counts that row. Count a NOT NULL column from the joined side instead — COUNT(o.order_id) — which returns 0.

open as a page

How does COUNT(*) FILTER (WHERE status = 'paid') differ from putting that predicate in the query's WHERE clause?

level: middleimportance: must knowfreq 45%

basics

~20 s

FILTER restricts the rows one aggregate sees, leaving every other aggregate and the grouping untouched. A WHERE predicate removes rows from the whole query, so it constrains every aggregate at once and can erase entire groups.

open as a page

How does GROUP BY treat rows whose grouping key is NULL, and how many groups do they form?

level: middleimportance: must knowfreq 60%

basics

~20 s

GROUP BY treats two NULLs as not distinct, so all rows with a NULL key collapse into one group whose key prints as NULL — even though NULL = NULL evaluates to UNKNOWN in a WHERE predicate.

open as a page

In a ROLLUP result, how do you tell a subtotal row's NULL from a NULL in the data?

level: middleimportance: must knowfreq 50%

basics

~10 s

Use the GROUPING() function: GROUPING(region) returns 1 when that row's grouping set left region out, and 0 when the row is genuinely grouped by region — including when the grouped value itself is NULL.

open as a page

In SQL, how do you aggregate two one-to-many child tables in one query without cross-inflating totals?

level: middleimportance: must knowfreq 62%

basics

~20 s

Aggregate each child separately first — one derived table or CTE per child, grouped by the parent key — then LEFT JOIN those one-row-per-parent summaries back to the parent. Joining both children directly multiplies their rows together.

open as a page

Why does SUM(amount) return NULL instead of 0 when no rows match the WHERE clause?

level: middleimportance: must knowfreq 68%

basics

~20 s

SUM over an empty input set is defined to return NULL, not 0 — there is nothing to add, and SQL reports "no value" rather than inventing a zero. Wrap it as COALESCE(SUM(amount), 0) when the caller needs a number.

open as a page

Why can STRING_AGG produce a different element order each run, and where does its ORDER BY go?

level: middleimportance: must knowfreq 50%

basics

~20 s

A group's rows have no inherent order, so concatenation order is unspecified unless you order inside the aggregate — WITHIN GROUP (ORDER BY …) or an ORDER BY in the argument list. The query's own ORDER BY sorts result rows, not the elements inside one value.

open as a page

Using FILTER (WHERE …), how do you return total, paid and refunded order counts per customer in one SELECT?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Group by customer and give each measure its own filter clause: COUNT() for the total, COUNT() FILTER (WHERE status = 'paid') and COUNT(*) FILTER (WHERE status = 'refunded'). One grouped pass produces all three columns.

open as a page

What extra rows does GROUP BY ROLLUP(region, product) add to a normal grouped result?

level: juniorimportance: should knowfreq 40%

basics

~10 s

It adds one subtotal row per region plus one grand-total row on top of the ordinary region-and-product groups. In those extra rows the columns rolled away come back as NULL placeholders.

open as a page

What do MIN and MAX return for VARCHAR and DATE columns, and what decides the order?

level: middleimportance: should knowfreq 44%

basics

~20 s

MIN and MAX return a value of the argument's own type — the earliest and latest DATE, the first and last string. Dates order chronologically; character data orders by the column's collation, so '9' sorts after '10'.

open as a page

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

level: middleimportance: should knowfreq 52%

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.

open as a page

What does SUM(DISTINCT amount) compute, and when would you actually want it?

level: middleimportance: should knowfreq 38%

basics

~20 s

SUM(DISTINCT amount) removes duplicate amount values within each group and adds what remains, so two rows of 50 contribute 50 once. It is right only when duplicate values are genuinely the same fact, and wrong for money totals.

open as a page

How do you pivot rows into one column per status without a PIVOT operator?

level: middleimportance: should knowfreq 55%

basics

~20 s

Group by the row key and write one conditional aggregate per output column: SUM(CASE WHEN status = 'new' THEN 1 ELSE 0 END) AS new_count, one per value. Use MAX(CASE …) when each cell holds a single value rather than a total.

open as a page

In one SQL query, how do you compute each customer's cancellation rate with conditional aggregation?

level: middleimportance: should knowfreq 48%

basics

~20 s

Divide a conditional sum by a total in the same GROUP BY: 100.0 * SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) / COUNT(*). Force numeric arithmetic with the 100.0 literal, and guard any conditional denominator with NULLIF.

open as a page

Does COUNT(1) ever return a different number from COUNT(*) in SQL?

level: middleimportance: should knowfreq 70%

basics

~20 s

No. COUNT(1) counts rows where the constant 1 is not NULL, and a constant is never NULL, so it counts every row — exactly what COUNT(*) does. The choice between them is style, not semantics.

open as a page

What does COUNT(DISTINCT status) return for four rows with statuses A, A, B and NULL?

level: middleimportance: should knowfreq 66%

basics

~10 s

Two. COUNT(DISTINCT expr) counts different non-NULL values, so A and B count once each and the NULL is ignored — it never forms a value of its own.

open as a page

Your engine rejects FILTER (WHERE …) on an aggregate — how do you rewrite it portably?

level: middleimportance: should knowfreq 50%

basics

~20 s

Move the predicate inside the aggregate as a CASE expression with no ELSE: COUNT(*) FILTER (WHERE c) becomes COUNT(CASE WHEN c THEN 1 END), and SUM(x) FILTER (WHERE c) becomes SUM(CASE WHEN c THEN x END). Non-matching rows become NULL and aggregates skip NULLs.

open as a page

With COUNT(*) FILTER (WHERE status = 'refunded'), what does a customer group with no refunds return?

level: middleimportance: should knowfreq 35%

basics

~20 s

The customer still appears, with 0. Grouping is decided before any filter clause runs, so the group survives; COUNT over no matching rows is 0, while SUM, AVG, MIN and MAX over no matching rows would be NULL.

open as a page

showing 1–30 of 56