COUNT, SUM, AVG, MIN, MAX
An operator that reads many rows and gives back one
Parse calls of the five aggregate functions, check where they may stand, compute each with a small state that starts, steps over every row and finishes, skip NULLs the way SQL does, and print AVG with two decimals.
- An aggregate folds many values into one: start, step for each row, finish
- NULLs are skipped by every aggregate except COUNT(*), and over no values the answer is NULL, not 0
- Without GROUP BY the whole result is one group, so there is always exactly one row
- A bare column next to an aggregate has no single value to show
- AVG is the first value with a fraction: a fixed-point number with two decimals
- Aggregate is a blocking operator, like Sort: it must see every row first
Why this stage exists
Every SELECT so far has given one output row per input row it kept. The questions people ask most often are not like that: how many orders today, what is the total, the average, the largest? Each reads many rows and answers with one value. These are aggregates, and they are the first operator whose output has nothing to do with the number of rows that went in.
An aggregate is a small machine with three moves. It starts empty, takes a step for each row, and finishes with its answer. COUNT adds one per step; SUM adds the value; MIN keeps the smallest it has seen. The Aggregate operator runs every aggregate of the query side by side over the same rows, and hands up one row of finished answers.
One group, one row
Without GROUP BY, which arrives in the next stage, all the rows the WHERE keeps are one group, and the result is one row. Always: even when no row is kept, select count(*) from t where id > 100 prints one row, 0. That is the difference between "there are none" and "there is no answer".
Because the result is one row, a column on its own has nothing to show: select name, count(*) from t asks for one name to stand for five rows. That is 01507. What can stand beside an aggregate is anything with one value: a literal, another aggregate, or an expression of them, such as sum(a) + count(*).
NULLs, and why SUM of nothing is not 0
Every aggregate except COUNT(*) skips NULLs. COUNT(c) counts the rows where c has a value, and AVG(c) divides by that count, not by the number of rows:
| c | COUNT(*) | COUNT(c) | SUM(c) | AVG(c) | MIN(c) | MAX(c) |
|---|---|---|---|---|---|---|
| 10, NULL, 20, NULL, 30 | 5 | 3 | 60 | 20.00 | 10 | 30 |
| NULL, NULL | 2 | 0 | NULL | NULL | NULL | NULL |
| no rows | 0 | 0 | NULL | NULL | NULL | NULL |
The bottom two rows are the ones to remember. A SUM over no values is NULL, not 0: there was nothing to add, and 0 would claim a total. COUNT is the exception because counting nothing does have an answer.
AVG and its two decimals
The engine has no fractional numbers: every value so far has been an INT, a VARCHAR or a truth value. The mean of 1 and 2 is not an INT, so AVG produces the one fractional value in this part, a number with exactly two decimals. Keep it as a whole number of hundredths, rounded half away from zero, and print it with a point: the mean of 1 and 2 is 1.50, of 1, 1 and 2 is 1.33, of 1, 2 and 2 is 1.67.
An AVG can be compared with an INT and sorted, which the next stage needs for having avg(salary) > 100. Arithmetic on it is 00403: a real DECIMAL type, with its own rules for scale and rounding, belongs to the optional SQL surface track.
Calls, and where they may stand
A call is a name directly followed by (. The name is not a keyword, so a column may still be called count: select count from k reads the column, and select count(count) from k counts it. The names are checked after parsing, in any case: COUNT, count and Count are the same function, and every other name is 01506.
Two places cannot hold an aggregate, and both are 01508. The WHERE decides about one row at a time, before any aggregate has a value. And an aggregate's argument is evaluated once per row, so it cannot itself be an aggregate: sum(count(*)) has no meaning without groups.
The algorithm in words
Keep one state per aggregate
Each aggregate is a tiny machine. The Aggregate operator runs all of them side by side over the same rows.
- 1.Start: COUNT at 0; SUM and AVG with a running total of 0 and a count of 0 values; MIN and MAX with no value yet.
- 2.Step, for each row the child hands up: evaluate the argument. If it is NULL, skip this row for this aggregate (COUNT(*) has no argument and counts every row).
- 3.Otherwise COUNT adds 1; SUM and AVG add the value to the total and 1 to their count; MIN and MAX keep the value if they have none yet or if it is smaller (or larger), compared as ORDER BY compares.
- 4.Finish, once the child has no more rows: COUNT is its count. SUM is its total, or NULL if it saw no value. MIN and MAX are their value, or NULL. AVG is NULL if it saw no value, otherwise the total divided by the count, to two decimals.
- 5.Invariant: the finished values do not depend on the order the rows came in.
Compute AVG to two decimals
There is no fractional type in this engine. AVG makes one, just big enough: a whole number of hundredths.
- 1.Multiply the total by 100 and divide by the count, keeping the remainder.
- 2.Round half away from zero: if twice the remainder is at least the count, move one hundredth further from zero.
- 3.Print the hundredths as a number with a point before its last two digits: 150 is 1.50, 133 is 1.33, -150 is -1.50, 500 is 5.00.
- 4.To compare it with an INT, compare it with that INT times 100.
Check where aggregates stand
An aggregate changes what the select list means, so the checks run before any row is read.
- 1.A name followed by ( is a call. It must be COUNT, SUM, AVG, MIN or MAX, with one argument, and * only in COUNT: otherwise 01506.
- 2.A call inside the WHERE, or inside another call's argument, is 01508: the WHERE runs on single rows, before there is anything to aggregate.
- 3.If the select list has a call, every column it uses must be inside a call: a bare column is 01507. The same holds for ORDER BY.
- 4.Then the types: SUM and AVG take INTs, MIN and MAX take INTs or VARCHARs, and AVG's result takes no arithmetic (00403).
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, section 47.
| Input | Output |
|---|---|
select count(*), count(c) from t; | COUNT(*) | COUNT(c), then one row, (1 rows) |
select avg(a) from t where id <= 2; (a = 1, 2) | AVG(a), then 1.50 |
select sum(c) from t where c is null; | SUM(c), then NULL |
explain select count(*), sum(c) + count(*) from t where a > 1; | Project (cols=[COUNT(*), SUM(c) + COUNT(*)]), Aggregate (aggs=[COUNT(*), SUM(c)]), Filter (a > 1), SeqScan (table=t) |
ast select count(*), Sum(a * c) as s from t; | Columns: [COUNT(*), SUM(a * c) AS s] |
select foo(a) from t;, select sum(a, c) from t;, select sum(*) from t; | [ERROR:01506] <message> |
select name, count(*) from t; | [ERROR:01507] <message> |
select id from t where count(*) > 1; | [ERROR:01508] <message> |
A call prints as its name in capitals and its argument as section 40 prints expressions, in ast, in plans and in headers. The Aggregate node lists each different call once, in the order the statement first writes it. rows_examined is the scan's: the Aggregate reads every row and adds nothing to the count.
What you should see when done
$droid --db shop.dbWelcome to droid!>create table orders (id int, qty int, price int);Create table statement executed successfully>insert into orders (id, qty, price) values (1, 2, 500);Insert statement executed successfully (1 rows)>insert into orders (id, qty, price) values (2, 1, 1200);Insert statement executed successfully (1 rows)>insert into orders (id, price) values (3, 300);Insert statement executed successfully (1 rows)>select count(*), count(qty), sum(qty * price) as total, avg(price) from orders;COUNT(*) | COUNT(qty) | total | AVG(price)3 | 2 | 2200 | 666.67(1 rows)[EXEC] rows_examined=3>select max(price) - min(price) from orders where qty is not null;MAX(price) - MIN(price)700(1 rows)[EXEC] rows_examined=3>select sum(qty) from orders where id > 10;SUM(qty)NULL(1 rows)[EXEC] rows_examined=0>select id, count(*) from orders;[ERROR:01507] Column id must appear in an aggregate>.exitexiting.. good bye!
Order 3 has no quantity, so it counts for COUNT(*) and not for COUNT(qty), and its product is NULL and left out of the total. The mean price, 2000 / 3, rounds to 666.67. A sum over no rows is NULL. The messages are yours to word; the tests check only the codes.
Common bugs in this stage
- No row for an empty input. The Aggregate must hand up its one row even when its child gave nothing.
- SUM starting at 0 and staying there. An empty or all-NULL SUM must be NULL; track whether it saw a value.
- AVG divided by the row count. Divide by the number of values that were not NULL.
- AVG truncated. 5 / 3 is 1.67, not 1.66; and 1.5 prints as
1.50, not1.5. - COUNT(c) counting NULLs. Only
COUNT(*)counts every row. - MIN over text compared as numbers, or case-folded. Compare bytes, as ORDER BY does.
- Calls recognised by keyword. Making
counta keyword breaks a column calledcount; a call is a name followed by(. - The bare-column check only looking at the top of an item.
count(*) + ahidesainside an expression; check every column outside a call.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C and C++: one struct per aggregate state (kind, count,
int64_ttotal, a value and a "has value" flag) and a switch on the kind for start, step and finish. Print hundredths with"%s%" PRId64 ".%02" PRId64, handling the sign yourself so-0.50keeps its minus. - Rust: an
enum AggStatewith one variant per function, andi64::checked_addfor SUM.i64::div_euclidis not the rounding you want; compute the remainder and round by hand. - Zig: a tagged union for the states;
@divTruncand@remgive the quotient and remainder to round from, andstd.fmtwith{d:0>2}pads the hundredths.
Checklist
One line per thing the tests check. The tags name the test cases.
- COUNT(*) counts rows; COUNT(c) counts the rows where c is not NULLcount-star-vs-col
- SUM, AVG, MIN and MAX skip NULLsaggregates-ignore-null
- Over values that are all NULL, SUM is NULL, not 0all-null-input
- No row in still gives one row outempty-input
- AVG of INTs prints two decimals, rounded half away from zeroavg-two-decimals
- MIN and MAX of text compare bytesmin-max-varchar
- Aggregates see only the rows the WHERE keepsagg-with-where
- An aggregate's argument is an expression, and aggregates can be combinedagg-expressions
- count is a name; only count( is a callcolumn-named-count
- A bare column next to an aggregate is 01507mixed-agg-and-column
- Calls are checked before any row is read: 01506, 00403, 01508agg-errors
- Aggregates are one Aggregate node, each call listed onceexplain-aggregate
- A call is a name and parentheses; ast prints it in capitalstokenize-ast-calls