What an Aggregate Actually Does
Most SQL functions you've met so far operate row-by-row: UPPER(name) runs once per row, ROUND(price, 2) runs once per row. Aggregate functions are different. They look at a whole set of rows and collapse it down to a single value.
Set up a small table to play with:
Five rows go in, one row comes out. That's the whole mental model: aggregates squash rows into a summary. Without a GROUP BY, the summary covers every row in the result.
COUNT: Rows vs Values
COUNT has three forms, and the difference matters:
COUNT(*)counts rows. NULLs included. Always returns a number.COUNT(column)counts non-NULL values in that column.COUNT(DISTINCT column)counts unique non-NULL values.
Five rows, three of them with an amount, three distinct customers. If you ever see COUNT(amount) and wonder why it's smaller than COUNT(*), that's why — NULLs aren't counted.
SUM, AVG, MIN, MAX
The arithmetic aggregates work as you'd expect, with one quiet rule: they all skip NULL:
AVG is (10 + 20 + 30) / 3 = 20.0, not 60 / 4 = 15.0. The denominator is the count of non-NULL values. If that's not what you want — if you'd rather treat missing data as zero — be explicit:
MIN and MAX work on text and dates too, comparing lexicographically for text and as ISO strings for the standard date format.
SUM vs TOTAL
SQLite has a second sum-like aggregate, TOTAL, that fixes two annoyances of SUM:
SUMof zero rows returnsNULL.TOTALreturns0.0.SUMof all-NULL values returnsNULL.TOTALreturns0.0.TOTALalways returns a floating-point number, so it never overflows integer arithmetic.
The trade-off: TOTAL is non-standard, and the always-REAL result can surprise you if you were expecting an integer. Reach for it when "no rows means zero" is the right answer for your app, and stick with SUM when you want SQL-standard behaviour.
DISTINCT Inside Aggregates
DISTINCT can go inside any aggregate, not just COUNT. It removes duplicate values before the aggregation runs:
SUM(amount) adds every row's amount. SUM(DISTINCT amount) adds each unique amount once — useful for things like "total of unique invoice amounts" but rarely what you want. COUNT(DISTINCT customer) is the common one.
FILTER: Aggregating a Subset
When you want to aggregate only some of the rows, the obvious move is WHERE. But WHERE filters everything — you can't combine "count paid orders" and "count refunds" in the same query that way. FILTER solves it:
Each FILTER (WHERE ...) clause applies only to that one aggregate. One pass over the table, multiple slices summarized. Before FILTER existed, people wrote SUM(CASE WHEN status = 'paid' THEN amount END) — same idea, more typing.
GROUP_CONCAT: Joining Strings
GROUP_CONCAT is the odd one out. Instead of returning a number, it concatenates the values into a single string:
The default separator is a comma. Pass a second argument to use something else. Order isn't guaranteed unless you wrap the call as GROUP_CONCAT(tag ORDER BY tag) — handy when the output appears in a UI and you want it stable.
Aggregating Without GROUP BY
Every example so far that used aggregates without GROUP BY produced exactly one row. That's the rule: a SELECT with aggregates and no GROUP BY is a single-row summary of the whole table (after WHERE).
You can mix aggregates freely:
What you can't do is mix non-aggregated columns with aggregates and expect sensible results:
-- Allowed by SQLite, but the value of `customer` is arbitrary.
SELECT customer, SUM(amount) FROM orders;
SQLite won't error here (other databases will), but it'll pick some random customer's name to show alongside the total. If you want a sum per customer, you need GROUP BY — which is the next page.
Next: GROUP BY and HAVING
Aggregates over the whole table answer "how much in total." Aggregates per group — per customer, per month, per status — answer the more interesting questions. GROUP BY is how you split the rows into buckets before aggregating, and HAVING is how you filter on the aggregated result. That's next.
Frequently Asked Questions
What are aggregate functions in SQLite?
Aggregate functions take many rows and return a single summary value. The built-in ones are COUNT, SUM, AVG, MIN, MAX, TOTAL, and GROUP_CONCAT. Without a GROUP BY, they collapse the entire result set into one row.
What's the difference between SUM and TOTAL in SQLite?
Both add numbers, but SUM returns NULL when every input is NULL and uses integer arithmetic when possible (which can overflow). TOTAL always returns a floating-point number and returns 0.0 when there are no rows. Use TOTAL when you want a guaranteed numeric result, SUM when SQL-standard behaviour matters.
How do I count distinct values in SQLite?
Put DISTINCT inside the call: COUNT(DISTINCT customer_id). That counts unique non-NULL values. Plain COUNT(column) counts non-NULL values including duplicates, and COUNT(*) counts every row regardless of NULLs.
Do SQLite aggregates ignore NULL?
Yes — every aggregate except COUNT(*) skips NULL inputs. AVG divides by the count of non-NULL values, not the total row count. COUNT(*) is the exception: it counts rows, not values, so NULLs are included.