10.10 Aggregate Functions
Table of Contents
Understanding Aggregate Functions
Aggregate functions help you calculate a single value from many rows. Instead of working on one row at a time, they look at a group of rows and return one result, for example a total, an average, or a maximum.
You will use aggregate functions constantly in reporting, dashboards, and analytics.
Typical aggregates include:
| Function | Description | Common use example |
|---|---|---|
COUNT | Number of rows or non-null values | How many users signed up |
SUM | Sum of numeric values | Total revenue |
AVG | Average of numeric values | Average order value |
MIN | Smallest value | First signup date |
MAX | Largest value | Biggest purchase amount |
All of these return one value for a set of rows.
Important rule:
Aggregate functions compute one result per group of rows.
If you use aggregate functions together with regular columns, you must use a GROUP BY clause or you will get an error in most SQL databases.
In the examples, imagine a table:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER,
amount NUMERIC(10, 2),
status VARCHAR(20), -- 'pending', 'paid', 'canceled'
created_at TIMESTAMP
);COUNT
COUNT returns how many rows match a condition.
Variants:
| Expression | What it counts |
|---|---|
COUNT(*) | All rows, including rows with NULL in some columns |
COUNT(column) | Rows where column is not NULL |
COUNT(DISTINCT column) | Unique, non-null values in column |
Basic COUNT examples
Count all orders:
SELECT COUNT(*) AS total_orders
FROM orders;Count all paid orders:
SELECT COUNT(*) AS paid_orders
FROM orders
WHERE status = 'paid';
Count users that have at least one order (ignores NULL user_id):
SELECT COUNT(user_id) AS users_with_orders
FROM orders;
If some rows have user_id = NULL, COUNT(user_id) will not include them.
Count distinct users who have made at least one order:
SELECT COUNT(DISTINCT user_id) AS unique_customers
FROM orders;COUNT with GROUP BY
Count orders per status:
SELECT status, COUNT(*) AS count_orders
FROM orders
GROUP BY status;Possible result:
| status | count_orders |
|---|---|
| pending | 12 |
| paid | 35 |
| canceled | 3 |
Each group of rows with the same status produces one count.
SUM
SUM adds up numeric values.
Total revenue from paid orders:
SELECT SUM(amount) AS total_revenue
FROM orders
WHERE status = 'paid';
If there are no matching rows, most databases return NULL for SUM(amount), not 0. You often want to handle this:
SELECT COALESCE(SUM(amount), 0) AS total_revenue
FROM orders
WHERE status = 'paid';SUM with GROUP BY
Revenue per user:
SELECT user_id, SUM(amount) AS user_revenue
FROM orders
WHERE status = 'paid'
GROUP BY user_id;Revenue per status:
SELECT status, SUM(amount) AS revenue
FROM orders
GROUP BY status;SUM with expressions
You can sum expressions, not only columns.
Sum of a discounted amount, for example 10 percent off:
SELECT SUM(amount * 0.9) AS discounted_revenue
FROM orders
WHERE status = 'paid';Sum of orders per day:
SELECT
DATE(created_at) AS order_date,
SUM(amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY DATE(created_at)
ORDER BY order_date;AVG
AVG calculates the average of numeric values.
Average order amount:
SELECT AVG(amount) AS avg_order_amount
FROM orders
WHERE status = 'paid';Average order amount per user:
SELECT user_id, AVG(amount) AS avg_order_amount
FROM orders
WHERE status = 'paid'
GROUP BY user_id;Average daily revenue:
SELECT
DATE(created_at) AS day,
AVG(amount) AS avg_order_amount
FROM orders
WHERE status = 'paid'
GROUP BY DATE(created_at)
ORDER BY day;
Like SUM, if there are no matching rows, AVG returns NULL.
Rule: AVG(column) ignores NULL values.
Only non-null values are included in the average.
Example:
- Values:
10, 20, NULL, 30 AVGuses only10, 20, 30, so the result is $(10 + 20 + 30) / 3 = 20$.
MIN and MAX
MIN returns the smallest value and MAX returns the largest.
First and last order dates:
SELECT
MIN(created_at) AS first_order,
MAX(created_at) AS last_order
FROM orders;Smallest and largest order amounts:
SELECT
MIN(amount) AS smallest_order,
MAX(amount) AS largest_order
FROM orders
WHERE status = 'paid';Per user:
SELECT
user_id,
MIN(amount) AS min_order_amount,
MAX(amount) AS max_order_amount
FROM orders
GROUP BY user_id;
MIN and MAX also ignore NULL values.
MIN and MAX with non-numeric columns
You can use them on dates and text.
Earliest and latest signup date:
SELECT
MIN(created_at) AS first_order,
MAX(created_at) AS last_order
FROM orders;Alphabetically first and last status:
SELECT
MIN(status) AS first_status,
MAX(status) AS last_status
FROM orders;This is not often useful for text, but it works.
Using Aggregate Functions with GROUP BY
Aggregate functions are often combined with GROUP BY. Every group produces one row.
General pattern:
SELECT
group_column_1,
group_column_2,
AGG_FUNC(column) AS some_value
FROM table
GROUP BY group_column_1, group_column_2;
Rule: Every column in SELECT that is not inside an aggregate function must appear in GROUP BY.
For example, revenue per day and status:
SELECT
DATE(created_at) AS day,
status,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM orders
GROUP BY DATE(created_at), status
ORDER BY day, status;Here:
DATE(created_at)andstatusare grouping columns.SUM(amount)andCOUNT(*)are aggregates.
If you tried to select amount directly, like:
SELECT DATE(created_at), amount, SUM(amount) FROM orders GROUP BY DATE(created_at);
Most databases would give an error, because amount is neither grouped nor aggregated.
Aggregate Functions with WHERE and HAVING
You can filter rows before aggregation with WHERE, and filter groups after aggregation with HAVING.
WHERE filters rows before aggregation
For example, only paid orders:
SELECT user_id, SUM(amount) AS paid_revenue
FROM orders
WHERE status = 'paid'
GROUP BY user_id;
The WHERE clause removes non-paid orders from the input.
HAVING filters groups after aggregation
For example, show only users whose total revenue is over 100:
SELECT user_id, SUM(amount) AS total_revenue
FROM orders
WHERE status = 'paid'
GROUP BY user_id
HAVING SUM(amount) > 100;
Here, HAVING uses SUM(amount), which is an aggregate result.
Filter days where there were at least 10 paid orders:
SELECT
DATE(created_at) AS day,
COUNT(*) AS orders_count
FROM orders
WHERE status = 'paid'
GROUP BY DATE(created_at)
HAVING COUNT(*) >= 10
ORDER BY day;Rule:
- Use
WHEREto filter individual rows before aggregates. - Use
HAVINGto filter whole groups after aggregates.
Combining Multiple Aggregates
You can use several aggregate functions in the same query.
Overall statistics:
SELECT
COUNT(*) AS total_orders,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount
FROM orders
WHERE status = 'paid';Per user:
SELECT
user_id,
COUNT(*) AS orders_count,
SUM(amount) AS total_spent,
AVG(amount) AS avg_order,
MIN(amount) AS smallest_order,
MAX(amount) AS largest_order
FROM orders
WHERE status = 'paid'
GROUP BY user_id;DISTINCT with Aggregate Functions
You can combine DISTINCT with some aggregates.
Count distinct users per status:
SELECT
status,
COUNT(DISTINCT user_id) AS unique_users
FROM orders
GROUP BY status;Average of distinct amounts:
SELECT AVG(DISTINCT amount) AS avg_unique_amount
FROM orders
WHERE status = 'paid';This ignores duplicate amounts before averaging.
Be careful: DISTINCT inside aggregates can be slower on large data, since the database must remove duplicates first.
Aggregate Functions and NULL Values
How aggregates treat NULL:
| Function | Handles NULL as |
|---|---|
COUNT(*) | Counts all rows, including rows with NULL columns |
COUNT(column) | Ignores rows where column is NULL |
SUM(column) | Ignores NULL values |
AVG(column) | Ignores NULL values |
MIN(column) | Ignores NULL values |
MAX(column) | Ignores NULL values |
Example table numbers:
| value |
|---|
| 10 |
| NULL |
| 20 |
Then:
COUNT(*)is3COUNT(value)is2SUM(value)is30AVG(value)is $30 / 2 = 15$MIN(value)is10MAX(value)is20
If a group has no non-null values, aggregates like SUM, AVG, MIN, MAX return NULL.
You can wrap them with COALESCE to replace NULL with a default:
SELECT
COALESCE(SUM(amount), 0) AS total_amount
FROM orders
WHERE status = 'non_existing_status';Practical Examples
Here are some realistic queries that use aggregate functions.
Daily revenue and order count
SELECT
DATE(created_at) AS day,
COUNT(*) AS orders_count,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_amount
FROM orders
WHERE status = 'paid'
GROUP BY DATE(created_at)
ORDER BY day;Top 5 customers by total revenue
SELECT
user_id,
COUNT(*) AS orders_count,
SUM(amount) AS total_revenue
FROM orders
WHERE status = 'paid'
GROUP BY user_id
ORDER BY total_revenue DESC
LIMIT 5;Number of orders and average amount per status
SELECT
status,
COUNT(*) AS orders_count,
AVG(amount) AS avg_amount
FROM orders
GROUP BY status;Users with at least 3 orders
SELECT
user_id,
COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 3;Percentage of revenue by status
Use aggregate functions in expressions:
SELECT
status,
SUM(amount) AS revenue,
ROUND(
SUM(amount) * 100.0 / SUM(SUM(amount)) OVER (),
2
) AS revenue_percent
FROM orders
GROUP BY status;Explanation, without going deep into window functions:
SUM(amount)is the revenue for each status.SUM(SUM(amount)) OVER ()is the total revenue across all statuses.- The division gives the share per status in percent.
Summary
Aggregate functions are central to reporting and analytics in SQL:
COUNTcounts rows or values.SUMadds values.AVGcomputes averages.MINandMAXfind smallest and largest values.- They work together with
GROUP BYto produce one result per group. WHEREfilters rows before aggregation,HAVINGfilters groups after aggregation.- Most aggregates ignore
NULL, exceptCOUNT(*), which counts every row.
You will combine these with GROUP BY, WHERE, HAVING, and joins to answer almost any statistical question about your data.
Views: 8
KAHIBARO