Aggregates, GROUP BY & HAVING
Aggregation changes the grain of a result. Instead of one row per source record, you can produce one row for the whole input or one row per group.
Core aggregate functions
COUNT counts rows or non-NULL expressions; SUM and AVG combine numeric values; MIN and MAX return extrema.
Run the query to see the result.
COUNT(*) includes every row. COUNT(column) ignores rows where that column is NULL.
GROUP BY defines output grain
Run the query to see the result.
Every selected non-aggregate expression should be functionally compatible with the grouping. Some engines are stricter than others; write SQL that makes the grouping intention explicit.
WHERE versus HAVING
WHERE filters individual rows before aggregation. HAVING filters groups after aggregation.
Run the query to see the result.
Use WHERE whenever the rule applies to source rows; use HAVING when the rule depends on an aggregate result.
Conditional aggregation
CASE inside an aggregate can produce multiple metrics in one pass.
Run the query to see the result.
Grouping across joins
When joining one-to-many tables before aggregating, be aware that the join changes row count. A customer joined to three orders appears three times before grouping. This is expected, but joining multiple one-to-many relationships at once can multiply rows and inflate sums.
Result-grain checklist
Before writing an aggregate query, state the desired grain in one sentence: “one row per customer,” “one row per build,” or “one row per day and status.” Then make GROUP BY match that sentence.
Practice: produce one row per test_results.build_id with total tests, passed tests, failed tests, and errors.