skip to content

Core Aggregate Functions

The standard aggregates — COUNT, SUM, AVG, MIN, MAX — and how their argument types, DISTINCT modifier, and result types behave. Interviewers use these as the vocabulary for every aggregation exercise, so I have to know their signatures and edge cases cold.

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

questions

6

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

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

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

Why can SUM over an INTEGER column overflow, and how do you prevent it portably?

level: seniorimportance: should knowfreq 30%

basics

~20 s

SUM derives its result type from its argument. Engines that keep INTEGER for INTEGER input can exceed that range on a large table and raise an arithmetic overflow. Prevent it by casting the argument to a wider type inside the aggregate.

open as a page