33 COUNT, SUM, AVG
Aggregate functions compute a single value across many rows. The five most common — count, sum, avg, min, max — are foundational. They behave a little differently with NULL: count(*) counts every row including those with NULLs; count(col) counts non-NULL values of col; sum/avg/min/max ignore NULLs entirely. There are dozens more (stddev, string_agg, array_agg, bool_and, bool_or, jsonb_agg, …), with behavior specific to each aggregate: array_agg and jsonb_agg, for example, retain NULL inputs.
What you’ll learn
count(*)vscount(col)vscount(DISTINCT col)sum,avg,min,maxon numeric and other types- How aggregates handle NULL
string_aggandarray_aggfor building listsFILTER (WHERE ...)for per-aggregate conditions
Example
count(*) is the row count — fast and clear. count(col) skips NULL values of col and is the right call when you want “how many rows have a non-NULL email?” count(DISTINCT col) deduplicates first; it is slower because it must materialize the distinct set.
sum returns the sum, or NULL if there are no rows (not 0 — surprising, but standard). avg returns NULL on no rows; on integer inputs, avg returns a numeric because the average of integers is rarely an integer. min and max work on anything orderable, including text and timestamps.
string_agg and array_agg are PostgreSQL’s “concatenate the values” aggregates — exactly the right tool when you want to render a list. The optional ORDER BY inside controls the order of concatenation, which the default unordered behavior cannot guarantee.
FILTER (WHERE ...) is the SQL-standard way to apply a per-aggregate condition. It replaces the older sum(CASE WHEN cond THEN x END) idiom — same result, much clearer.
Run the example
To run:
Common pitfalls
sumof zero rows returnsNULL, not0. UseCOALESCE(sum(x), 0)if you need 0.avgon an integer column produces anumericresult; if you want a rounded display, wrap inround.count(col)skipping NULLs is a feature, but it surprises beginners.count(*)is the row count; everything else is “non-null count.”- Aggregates without
GROUP BYcollapse the entire result to one row. WithGROUP BYyou get one row per group (next lesson).
Combine aggregates and FILTER in a single pass to get many statistics at once. SELECT count(*), count(*) FILTER (WHERE is_active), avg(amount) FILTER (WHERE status = 'paid'), max(created_at) FROM ... is one scan, many answers.
Add a price column to fruits with ALTER TABLE fruits ADD COLUMN price numeric(10,2); (then UPDATE some prices), and run sum, avg, min, max on it. Then try SELECT string_agg(name, ', ' ORDER BY id DESC) FROM fruits; and see the order reverse.
Downloads
SQL file: count-sum-avg.sql · All lesson files · Setup guide