Aggregations & Grouping
COUNT, SUM, AVG, and GROUP BY.
Analyzing Data with Aggregates
Aggregate functions perform calculations across multiple rows and return a single result. Instead of retrieving individual records, you can answer questions like "What's the total?", "What's the average?", or "How many are there?". Combined with GROUP BY, aggregates let you analyze data by categories, the foundation of business intelligence and reporting.
Sample Data: sales
We'll analyze this sales table throughout the lesson:
┌──────────┬────────────┬─────────────┬────────┬──────────┐ │ sale_id │ product │ category │ amount │ quantity │ ├──────────┼────────────┼─────────────┼────────┼──────────┤ │ 1 │ Laptop │ Electronics │ 1200 │ 1 │ │ 2 │ Mouse │ Electronics │ 25 │ 3 │ │ 3 │ Desk │ Furniture │ 350 │ 1 │ │ 4 │ Chair │ Furniture │ 200 │ 2 │ │ 5 │ Monitor │ Electronics │ 400 │ 2 │ │ 6 │ Lamp │ Furniture │ 45 │ 1 │ │ 7 │ Keyboard │ Electronics │ 75 │ 2 │ └──────────┴────────────┴─────────────┴────────┴──────────┘
COUNT: Counting Rows
COUNT returns the number of rows that match your criteria.
Count All Rows
How many sales do we have?
SELECT COUNT(*) AS total_sales FROM sales;
Expected Output:
┌─────────────┐ │ total_sales │ ├─────────────┤ │ 7 │ └─────────────┘
Count Specific Column
Count non-NULL values in a column (same as COUNT(*) if no NULLs exist).
SELECT COUNT(product) AS product_count FROM sales;
Expected Output:
┌───────────────┐ │ product_count │ ├───────────────┤ │ 7 │ └───────────────┘
Count with WHERE
How many Electronics sales?
SELECT COUNT(*) AS electronics_sales FROM sales WHERE category = 'Electronics';
Expected Output:
┌───────────────────┐ │ electronics_sales │ ├───────────────────┤ │ 4 │ └───────────────────┘
Count DISTINCT Values
How many unique categories exist?
SELECT COUNT(DISTINCT category) AS unique_categories FROM sales;
Expected Output:
┌───────────────────┐ │ unique_categories │ ├───────────────────┤ │ 2 │ └───────────────────┘
SUM: Adding Values
SUM adds up numeric values in a column.
Total Revenue
What's the total sales amount?
SELECT SUM(amount) AS total_revenue FROM sales;
Expected Output:
┌───────────────┐ │ total_revenue │ ├───────────────┤ │ 2295 │ └───────────────┘
Sum with WHERE
Total revenue from Furniture only.
SELECT SUM(amount) AS furniture_revenue FROM sales WHERE category = 'Furniture';
Expected Output:
┌───────────────────┐ │ furniture_revenue │ ├───────────────────┤ │ 595 │ └───────────────────┘
AVG: Average Values
AVG calculates the mean (average) of numeric values.
Average Sale Amount
What's the average transaction value?
SELECT AVG(amount) AS average_sale FROM sales;
Expected Output:
┌──────────────────────┐ │ average_sale │ ├──────────────────────┤ │ 327.8571428571428571 │ └──────────────────────┘
Rounded Average
Round to 2 decimal places for cleaner output.
SELECT ROUND(AVG(amount), 2) AS average_sale FROM sales;
Expected Output:
┌──────────────┐ │ average_sale │ ├──────────────┤ │ 327.86 │ └──────────────┘
MIN & MAX: Finding Extremes
MIN and MAX find the smallest and largest values.
Lowest and Highest Sale
SELECT
MIN(amount) AS lowest_sale,
MAX(amount) AS highest_sale
FROM sales;Expected Output:
┌─────────────┬──────────────┐ │ lowest_sale │ highest_sale │ ├─────────────┼──────────────┤ │ 25 │ 1200 │ └─────────────┴──────────────┘
Combining Multiple Aggregates
You can use multiple aggregate functions in one query.
Complete Sales Summary
SELECT
COUNT(*) AS total_sales,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_sale,
MIN(amount) AS min_sale,
MAX(amount) AS max_sale
FROM sales;Expected Output:
┌─────────────┬───────────────┬──────────────────────┬──────────┬──────────┐ │ total_sales │ total_revenue │ avg_sale │ min_sale │ max_sale │ ├─────────────┼───────────────┼──────────────────────┼──────────┼──────────┤ │ 7 │ 2295 │ 327.8571428571428571 │ 25 │ 1200 │ └─────────────┴───────────────┴──────────────────────┴──────────┴──────────┘
GROUP BY: Aggregating by Category
GROUP BY splits data into groups and calculates aggregates for each group separately. This is where aggregates become truly powerful.
Sales Count by Category
How many sales in each category?
SELECT
category,
COUNT(*) AS sales_count
FROM sales
GROUP BY category;Expected Output:
┌─────────────┬─────────────┐ │ category │ sales_count │ ├─────────────┼─────────────┤ │ Furniture │ 3 │ │ Electronics │ 4 │ └─────────────┴─────────────┘
Revenue by Category
Total revenue per category.
SELECT
category,
SUM(amount) AS total_revenue
FROM sales
GROUP BY category;Expected Output:
┌─────────────┬───────────────┐ │ category │ total_revenue │ ├─────────────┼───────────────┤ │ Furniture │ 595 │ │ Electronics │ 1700 │ └─────────────┴───────────────┘
Multiple Aggregates with GROUP BY
Complete breakdown per category.
SELECT
category,
COUNT(*) AS num_sales,
SUM(amount) AS total,
AVG(amount) AS avg_sale
FROM sales
GROUP BY category;Expected Output:
┌─────────────┬───────────┬───────┬──────────────────────┐ │ category │ num_sales │ total │ avg_sale │ ├─────────────┼───────────┼───────┼──────────────────────┤ │ Furniture │ 3 │ 595 │ 198.3333333333333333 │ │ Electronics │ 4 │ 1700 │ 425.0000000000000000 │ └─────────────┴───────────┴───────┴──────────────────────┘
How GROUP BY Works
1. Splits rows into groups based on GROUP BY column(s)
2. Calculates aggregate function for each group separately
3. Returns one row per group
Sorting Grouped Results
Combine GROUP BY with ORDER BY to sort the aggregated results.
Sort by Revenue (Highest First)
SELECT
category,
SUM(amount) AS total_revenue
FROM sales
GROUP BY category
ORDER BY total_revenue DESC;Expected Output:
┌─────────────┬───────────────┐ │ category │ total_revenue │ ├─────────────┼───────────────┤ │ Electronics │ 1700 │ │ Furniture │ 595 │ └─────────────┴───────────────┘
HAVING: Filtering Grouped Results
WHERE filters rows before grouping. HAVING filters groups after aggregation. Use HAVING when you need to filter based on aggregate results.
Categories with High Revenue
Show only categories with total revenue over $600.
SELECT
category,
SUM(amount) AS total_revenue
FROM sales
GROUP BY category
HAVING SUM(amount) > 600;Expected Output:
┌─────────────┬───────────────┐ │ category │ total_revenue │ ├─────────────┼───────────────┤ │ Electronics │ 1700 │ └─────────────┴───────────────┘
Categories with Multiple Sales
Show categories with more than 2 sales.
SELECT
category,
COUNT(*) AS num_sales
FROM sales
GROUP BY category
HAVING COUNT(*) > 2;Expected Output:
┌─────────────┬───────────┐ │ category │ num_sales │ ├─────────────┼───────────┤ │ Furniture │ 3 │ │ Electronics │ 4 │ └─────────────┴───────────┘
WHERE vs HAVING
- WHERE: Filters individual rows before grouping
- HAVING: Filters groups after aggregation
- Can use both: WHERE first, then GROUP BY, then HAVING
Combining WHERE, GROUP BY, and HAVING
Use all three together for powerful filtering and analysis.
Complex Example
Find categories (excluding low-value items) with average sale over $150.
SELECT
category,
COUNT(*) AS num_sales,
AVG(amount) AS avg_sale
FROM sales
WHERE amount > 50
GROUP BY category
HAVING AVG(amount) > 150;Expected Output:
┌─────────────┬───────────┬──────────────────────┐ │ category │ num_sales │ avg_sale │ ├─────────────┼───────────┼──────────────────────┤ │ Furniture │ 2 │ 275.0000000000000000 │ │ Electronics │ 3 │ 558.3333333333333333 │ └─────────────┴───────────┴──────────────────────┘
Query Execution Order: 1. FROM - Get table 2. WHERE - Filter rows (amount > 50) 3. GROUP BY- Create groups 4. HAVING - Filter groups (AVG > 150) 5. SELECT - Calculate aggregates 6. ORDER BY- Sort results (if present)
GROUP BY Multiple Columns
You can group by multiple columns to create more detailed breakdowns.
Extended sales table with dates:
┌──────────┬─────────────┬────────────┬────────┐ │ sale_id │ category │ sale_date │ amount │ ├──────────┼─────────────┼────────────┼────────┤ │ 1 │ Electronics │ 2024-01-15 │ 1200 │ │ 2 │ Electronics │ 2024-01-15 │ 25 │ │ 3 │ Furniture │ 2024-01-15 │ 350 │ │ 4 │ Furniture │ 2024-01-16 │ 200 │ │ 5 │ Electronics │ 2024-01-16 │ 400 │ └──────────┴─────────────┴────────────┴────────┘
Group by Category AND Date
Daily revenue per category.
SELECT
category,
sale_date,
SUM(amount) AS daily_revenue
FROM sales
GROUP BY category, sale_date
ORDER BY sale_date, category;Expected Output:
┌─────────────┬────────────┬───────────────┐ │ category │ sale_date │ daily_revenue │ ├─────────────┼────────────┼───────────────┤ │ Electronics │ 2024-01-15 │ 1225 │ │ Furniture │ 2024-01-15 │ 350 │ │ Electronics │ 2024-01-16 │ 400 │ │ Furniture │ 2024-01-16 │ 200 │ └─────────────┴────────────┴───────────────┘
Aggregates with JOINs
Combine aggregates with joins for powerful multi-table analysis.
Two tables:
customers: ┌─────────────┬────────┐ │ customer_id │ name │ ├─────────────┼────────┤ │ 1 │ Alice │ │ 2 │ Bob │ │ 3 │ Charlie│ └─────────────┴────────┘ orders: ┌──────────┬─────────────┬────────┐ │ order_id │ customer_id │ amount │ ├──────────┼─────────────┼────────┤ │ 101 │ 1 │ 250 │ │ 102 │ 2 │ 180 │ │ 103 │ 1 │ 100 │ │ 104 │ 1 │ 75 │ └──────────┴─────────────┴────────┘
Customer Order Summary
Total orders and revenue per customer.
SELECT
c.name,
COUNT(o.order_id) AS total_orders,
SUM(o.amount) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name
ORDER BY total_spent DESC;Expected Output:
┌─────────┬──────────────┬─────────────┐ │ name │ total_orders │ total_spent │ ├─────────┼──────────────┼─────────────┤ │ Charlie │ 0 │ │ │ Alice │ 3 │ 425 │ │ Bob │ 1 │ 180 │ └─────────┴──────────────┴─────────────┘
ORDER BY total_spent DESC, yet Charlie is on top with no value at all. Charlie has no orders, so SUM returned NULL, and PostgreSQL sorts NULLs first in descending order (last in ascending). A NULL is not zero, it is unknown, so it cannot be ranked against numbers. Two fixes: wrap the sum in COALESCE(SUM(o.amount), 0) so the group really is zero, or spell out the placement with ORDER BY total_spent DESC NULLS LAST.total_orders is 0, not NULL.COUNT(o.order_id) counts non-NULL values, and Charlie's single NULL-padded row contributes nothing, giving 0. Had we writtenCOUNT(*) it would have counted that padded row and reported 1, which is the classic way to overcount customers who ordered nothing.Common Mistakes
❌ Selecting Non-Grouped Columns
-- WRONG: product isn't in GROUP BY SELECT category, product, SUM(amount) FROM sales GROUP BY category; -- CORRECT: Only group columns or aggregates SELECT category, SUM(amount) FROM sales GROUP BY category;
❌ Using WHERE Instead of HAVING
-- WRONG: Can't use aggregate in WHERE SELECT category, SUM(amount) FROM sales WHERE SUM(amount) > 500 GROUP BY category; -- CORRECT: Use HAVING for aggregates SELECT category, SUM(amount) FROM sales GROUP BY category HAVING SUM(amount) > 500;
❌ Forgetting GROUP BY with Aggregates
-- WRONG: Mixing aggregate and non-aggregate SELECT category, SUM(amount) FROM sales; -- CORRECT: Group by non-aggregate columns SELECT category, SUM(amount) FROM sales GROUP BY category;
Practical Example: Sales Report
Complete sales analysis with all concepts combined.
SELECT
category,
COUNT(*) AS num_sales,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_sale,
MIN(amount) AS lowest_sale,
MAX(amount) AS highest_sale
FROM sales
WHERE amount > 0
GROUP BY category
HAVING COUNT(*) >= 2
ORDER BY total_revenue DESC;Expected Output:
┌─────────────┬───────────┬───────────────┬──────────────────────┬─────────────┬──────────────┐ │ category │ num_sales │ total_revenue │ avg_sale │ lowest_sale │ highest_sale │ ├─────────────┼───────────┼───────────────┼──────────────────────┼─────────────┼──────────────┤ │ Electronics │ 4 │ 1700 │ 425.0000000000000000 │ 25 │ 1200 │ │ Furniture │ 3 │ 595 │ 198.3333333333333333 │ 45 │ 350 │ └─────────────┴───────────┴───────────────┴──────────────────────┴─────────────┴──────────────┘
Key Takeaways
- COUNT counts rows (use COUNT(*) or COUNT(column))
- SUM adds up numeric values
- AVG calculates the mean/average
- MIN/MAX find smallest/largest values
- GROUP BY splits data into groups for separate aggregation
- HAVING filters groups (use after GROUP BY)
- WHERE filters rows before grouping
- Execution order: WHERE → GROUP BY → HAVING → SELECT → ORDER BY
- Aggregates with GROUP BY are the foundation of business reporting and analytics, they turn raw data into actionable insights