4.4.8 · D4Databases

Exercises — Aggregate functions — COUNT, SUM, AVG, MIN, MAX

2,847 words13 min readBack to topic

The table below (sales) is used by most problems. Read it once now — a blank cell means NULL ("unknown / no value"), not zero.

id region rep amount
1 East Alice 100
2 East Bob 200
3 East Alice NULL
4 West Carol 300
5 West Carol 300
6 West Dan NULL
7 North Eve 50

Quick reminders you will need repeatedly, each stated in plain words:

  • COUNT(*) = count the rows themselves — a row existing is a fact, so NULL cells are still counted.
  • COUNT(col) = count only the rows where col is not NULL.
  • SUM, AVG, MIN, MAX all skip NULL silently.
  • AVG(col) = (sum of non-NULL values) ÷ (count of non-NULL values). It never divides by the blank rows.
  • Over a set with zero non-NULL values: SUM/AVG/MIN/MAX return NULL, but COUNT returns 0.
Figure — Aggregate functions — COUNT, SUM, AVG, MIN, MAX

The figure above shows the amount column as a vertical strip. Notice the two greyed cells (rows 3 and 6) — the arrows show which functions walk past them.


Level 1 — Recognition

You only need to pick the right function or read a value straight off the table.

Recall Solution L1.1

WHAT the question asks: "how many rows exist" — existence of a row, regardless of whether its amount cell is filled. WHY COUNT(*): the star means "count the rows themselves." COUNT(amount) would drop the two NULL-amount rows, giving the wrong answer to this question. Answer: COUNT(*) → there are 7 rows.

Recall Solution L1.2

WHAT we do: scan the amount column, ignoring the two NULLs (rows 3 and 6, which are "unknown" — you cannot say an unknown is the smallest or largest honestly). Remaining values: . MIN = smallest = 50. MAX = largest = 300.

Recall Solution L1.3
  • Filtering groups by an aggregate → ==HAVING==.
  • Filtering individual rows before grouping → ==WHERE==. WHY the split exists: in the pipeline , WHERE runs before the aggregate exists, so it literally cannot see SUM(amount). HAVING runs after, so it can. (See SQL query execution order.)

Level 2 — Application

Now you compute concrete numbers.

Recall Solution L2.1

WHAT we do, step by step:

  • COUNT(*) counts rows → 7.
  • COUNT(amount) counts non-NULL amounts → rows 3 and 6 dropped → 5.
  • SUM(amount) adds non-NULL values: 950.
  • AVG(amount) = SUM ÷ COUNT(amount) = 190. WHY divide by 5, not 7? AVG divides by the count of non-NULL values. The two blanks are invisible. If you wrongly divide by 7 you get — that is the classic error.
Recall Solution L2.2

WHAT COALESCE(amount,0) does: it replaces each NULL with 0 before the aggregate sees it (see COALESCE and NULLIF). So the two blanks become real zero values. Now the column is 7 non-NULL values. SUM is unchanged ( adds nothing) = , but the count is now 7. AVG = . WHY different: turning NULL into 0 makes those rows count toward the denominator. Use this only when "missing" genuinely means "zero sales".

Recall Solution L2.3

WHAT GROUP BY region does: it splits sales into mini-tables — one per distinct region — then runs the aggregate inside each.

region rows amounts (non-NULL) SUM COUNT AVG
East 1,2,3 {100, 200} 300 2 150
West 4,5,6 {300, 300} 600 2 300
North 7 {50} 50 1 50

WHY East's AVG = 150 not 100: East has 3 rows but only 2 non-NULL amounts (), so . Row 3's NULL is skipped in both SUM and COUNT.


Level 3 — Analysis

Now the tricky NULL, DISTINCT, and empty-set behaviours.

Recall Solution L3.1

WHAT DISTINCT does inside COUNT: it removes duplicates first, then counts what remains — and NULL is still excluded (see DISTINCT keyword).

  • rep values: Alice, Bob, Alice, Carol, Carol, Dan, Eve → distinct set = {Alice, Bob, Carol, Dan, Eve} → 5.
  • amount values (NULLs dropped): 100, 200, 300, 300, 50 → distinct set = {100, 200, 300, 50} → 4 (the two 300s collapse to one). WHY 4 not 5: the duplicate 300 (rows 4 and 5) is counted once by DISTINCT.
Recall Solution L3.2

North has one row (id 7, amount 50): SUM = 50, AVG = 50, MIN = 50, COUNT(amount) = 1.

South (zero non-NULL rows) — the important case:

  • SUM(amount)NULL (there is nothing to add; the empty sum returns NULL, not 0, in SQL).
  • AVG(amount)NULL ( is undefined, so SQL returns NULL).
  • MIN(amount)NULL (no value to be smallest).
  • COUNT(amount)0 (COUNT is the one that returns a number, never NULL). WHY this asymmetry: COUNT answers "how many?" — the honest answer to an empty set is zero. SUM/AVG/MIN/MAX answer "what value?" — and there is no value, so NULL ("unknown") is the honest answer. To force 0, use COALESCE(SUM(amount), 0).
Recall Solution L3.3

WHAT we compare per region: COUNT(*) (rows) vs COUNT(DISTINCT rep) (unique reps).

region COUNT(*) reps COUNT(DISTINCT rep) equal?
East 3 Alice,Bob,Alice 2 no — Alice repeats
West 3 Carol,Carol,Dan 2 no — Carol repeats
North 1 Eve 1 yes

WHY it matters: if these two counts differ, a rep appears more than once in that region — a useful data-quality signal.


Level 4 — Synthesis

Combine WHERE, GROUP BY, HAVING, and aliases correctly.

Recall Solution L4.1
SELECT region, AVG(amount) AS avg_amt
FROM sales
GROUP BY region
HAVING AVG(amount) > 100;

WHY HAVING not WHERE: the condition uses an aggregate (AVG), which does not exist until after grouping. WHERE runs earlier and would error. From L2.3 the averages were East 150, West 300, North 50. Keep those with average > 100:

region avg_amt
East 150
West 300

North (50) is dropped.

Recall Solution L4.2
SELECT region, SUM(amount) AS big_total
FROM sales
WHERE amount >= 100
GROUP BY region;

WHY WHERE here (not HAVING): we filter individual rows by a plain column condition before grouping. WHERE is the correct, earlier stage. Also note: WHERE amount >= 100 drops NULL amounts automatically — a comparison with NULL is never true (see NULL handling in SQL). So rows 3, 6 (NULL) and row 7 (50) are excluded. Surviving rows: East {100,200}, West {300,300}.

region big_total
East 300
West 600

(North has no row ≥ 100, so it disappears from the result entirely.)

Recall Solution L4.3

Two separate errors:

  1. WHERE a > 100 references an aggregate condition — but WHERE runs before aggregation, so AVG(amount) isn't computed yet. → must be HAVING.
  2. It also references the alias a — but SELECT (where aliases are born) runs after WHERE in the pipeline , so WHERE cannot see a even if it were a plain column. Fix:
SELECT region, AVG(amount) AS a
FROM sales
GROUP BY region
HAVING AVG(amount) > 100;

Result is the same as L4.1: East 150, West 300.


Level 5 — Mastery

Predict exact outputs and reason about edge behaviour.

Recall Solution L5.1

Work region by region, ORDER BY region gives alphabetical order East, North, West.

region rows_cnt amt_cnt s a s0
East 3 2 300 150 300
North 1 1 50 50 50
West 3 2 600 300 600

Key checks: East has 3 rows but 2 non-NULL amounts → amt_cnt=2, a = 300/2 = 150. Since no region here is all-NULL, s0 equals s everywhere (COALESCE only changes an all-NULL group's NULL into 0).

Recall Solution L5.2
  • SUM(amount) = 950, COUNT(*) = 7. This is "total divided by all rows", treating the two NULLs as if they were 0-value sales.
  • AVG(amount) = 950/5 = 190$. This divides by the **5 known** sales. **WHICH is right for "average among known sales":** AVG(amount)` (190), because it ignores the unknowns. The figure silently assumes the blanks are zero — only valid if a missing amount truly means "no sale of value 0".
Recall Solution L5.3
SELECT rep,
       COUNT(*)                 AS n_rows,
       COALESCE(SUM(amount), 0) AS total
FROM sales
GROUP BY rep
HAVING COUNT(*) > 1
ORDER BY rep;

WHY HAVING COUNT(*) > 1: we filter groups by row count — an aggregate condition, so it belongs in HAVING. Group by rep:

  • Alice: rows 1,3 → amounts {100, NULL} → n=2, SUM=100.
  • Bob: row 2 → n=1 → dropped (not > 1).
  • Carol: rows 4,5 → {300,300} → n=2, SUM=600.
  • Dan: row 6 → n=1 → dropped.
  • Eve: row 7 → n=1 → dropped.
rep n_rows total
Alice 2 100
Carol 2 600

Alice's total is 100 (her NULL row is skipped by SUM); COALESCE wasn't needed here since she has one non-NULL amount, but it protects any rep who happened to be all-NULL.



Connections

  • GROUP BY and HAVING — every L3–L5 problem depends on grouping then filtering.
  • NULL handling in SQL — the engine of nearly every trap here.
  • COALESCE and NULLIF — turning NULL into 0 in L2.2, L5.1, L5.3.
  • SQL query execution order — why L4.3 fails.
  • DISTINCT keyword — L3.1, L3.3.
  • Window functions — next step: aggregates that don't collapse rows.