๐Ÿ“Š Aggregations & Grouping

COUNT, SUM, AVG, MIN, MAX โ€” then GROUP BY and HAVING to slice data any way you want.

๐Ÿ“ฆ Our Practice Dataset โ€” The Orders Table

Throughout this entire lesson we'll work with a single orders table that tracks purchases in an online store. Think of it like the receipt drawer at a shop โ€” every sale gets a row.

๐Ÿง’ ELI5 โ€” Why One Table for Everything?

Imagine you run a lemonade stand. Every time somebody buys something, you write it down on a piece of paper: who bought it, what they bought, how many, and how much they paid. At the end of the day you have a long list. That list is our orders table! Later in this lesson we'll learn how to answer questions like "How much money did I make today?" or "Which flavour sold the most?" โ€” all from that one list.

Run the following SQL to create and populate the table:

-- Step 1: Create the orders table
CREATE TABLE orders (
    id          SERIAL PRIMARY KEY,
    customer    VARCHAR(100),
    product     VARCHAR(100),
    category    VARCHAR(50),
    amount      DECIMAL(10,2),
    quantity    INTEGER,
    order_date  DATE
);

-- Step 2: Insert 18 sample orders
INSERT INTO orders (customer, product, category, amount, quantity, order_date) VALUES
    ('Alice',   'Laptop',          'Electronics', 1200.00, 1,  '2024-01-15'),
    ('Alice',   'Wireless Mouse',   'Electronics', 25.99,   2,  '2024-01-15'),
    ('Alice',   'Python Book',      'Books',       39.99,   1,  '2024-02-10'),
    ('Bob',     'Headphones',       'Electronics', 79.99,   1,  '2024-01-20'),
    ('Bob',     'Running Shoes',    'Clothing',    89.99,   1,  '2024-02-05'),
    ('Bob',     'SQL Book',         'Books',       29.99,   1,  '2024-02-14'),
    ('Bob',     'T-Shirt',          'Clothing',    19.99,   3,  '2024-03-01'),
    ('Charlie', 'Keyboard',         'Electronics', 149.99,  1,  '2024-01-25'),
    ('Charlie', 'Monitor',          'Electronics', 349.99,  2,  '2024-02-18'),
    ('Charlie', 'Desk Lamp',        'Home',        45.00,   1,  '2024-03-10'),
    ('Diana',   'Yoga Mat',         'Clothing',    29.99,   1,  '2024-01-08'),
    ('Diana',   'Water Bottle',     'Home',        15.00,   4,  '2024-02-22'),
    ('Diana',   'Backpack',         'Clothing',    59.99,   1,  '2024-03-05'),
    ('Diana',   'Coffee Beans',     'Food',        14.99,   2,  '2024-03-12'),
    ('Eve',     'Tablet',           'Electronics', 499.99,  1,  '2024-02-01'),
    ('Eve',     'Phone Case',       'Electronics', 12.99,   2,  '2024-02-01'),
    ('Eve',     'Cookbook',          'Books',       NULL,    1,  '2024-03-15'),
    ('Alice',   'Sneakers',         'Clothing',    75.00,   1,  '2024-03-20');

Here's what our data looks like:

idcustomerproductcategoryamountqtyorder_date
1AliceLaptopElectronics1200.0012024-01-15
2AliceWireless MouseElectronics25.9922024-01-15
3AlicePython BookBooks39.9912024-02-10
4BobHeadphonesElectronics79.9912024-01-20
5BobRunning ShoesClothing89.9912024-02-05
6BobSQL BookBooks29.9912024-02-14
7BobT-ShirtClothing19.9932024-03-01
8CharlieKeyboardElectronics149.9912024-01-25
9CharlieMonitorElectronics349.9922024-02-18
10CharlieDesk LampHome45.0012024-03-10
11DianaYoga MatClothing29.9912024-01-08
12DianaWater BottleHome15.0042024-02-22
13DianaBackpackClothing59.9912024-03-05
14DianaCoffee BeansFood14.9922024-03-12
15EveTabletElectronics499.9912024-02-01
16EvePhone CaseElectronics12.9922024-02-01
17EveCookbookBooksNULL12024-03-15
18AliceSneakersClothing75.0012024-03-20

๐Ÿ’ก Pro Tip โ€” Notice the NULL!

Order #17 (the Cookbook) has a NULL amount โ€” maybe it was a free promotional copy or the price hasn't been entered yet. We deliberately included this because aggregate functions treat NULLs in surprising ways. Keep an eye on it throughout the lesson!

1๏ธโƒฃ What Are Aggregate Functions?

๐Ÿง’ ELI5 โ€” Summarising vs. Listing

Your teacher asks: "How many students are in this class?" You answer "30". That's a summary โ€” one number that describes the whole group. Now your teacher asks: "Tell me every student's name." Now you have to read 30 names. That's a list. Regular SELECT gives you the list. Aggregate functions give you the summary. They crunch many rows down into a single answer.

PostgreSQL has five core aggregate functions. Think of them as five super-powers for summarising data:

FunctionWhat It DoesLayman Translation
COUNT()Counts rows"How many?"
SUM()Adds up values"What's the total?"
AVG()Calculates the mean"What's the average?"
MIN()Finds the smallest value"What's the lowest?"
MAX()Finds the largest value"What's the highest?"

๐ŸŒ Real-Life Analogy

Imagine a basket of apples. COUNT = "How many apples are in the basket?" SUM = "What's the total weight of all the apples?" AVG = "What's the average weight per apple?" MIN = "Which apple weighs the least?" MAX = "Which apple weighs the most?" One basket, five completely different questions โ€” and aggregate functions answer all of them.

Let's start with the simplest one and build up.

2๏ธโƒฃ COUNT โ€” How Many Rows?

COUNT is probably the aggregate function you'll use the most. It answers one simple question: "How many?"

COUNT(*) โ€” Count All Rows

The asterisk * means "everything." COUNT(*) counts every row in the result set, including rows with NULLs.

-- How many orders do we have in total?
SELECT COUNT(*) AS total_orders
FROM orders;

Result:

total_orders
18

COUNT(column) โ€” Count Non-NULL Values

When you give COUNT a specific column name, it counts only the rows where that column is not NULL.

-- How many orders have an amount recorded?
SELECT COUNT(amount) AS orders_with_amount
FROM orders;

Result:

orders_with_amount
17

We get 17 instead of 18 because order #17 (the Cookbook) has a NULL amount. COUNT(amount) silently skips it!

COUNT(DISTINCT column) โ€” Count Unique Values

-- How many unique customers have placed orders?
SELECT COUNT(DISTINCT customer) AS unique_customers
FROM orders;

Result:

unique_customers
5

Even though there are 18 rows, only 5 different customers placed those orders: Alice, Bob, Charlie, Diana, and Eve.

-- How many different categories do we sell?
SELECT COUNT(DISTINCT category) AS category_count
FROM orders;

Result:

category_count
5

Five categories: Electronics, Books, Clothing, Home, and Food.

โš ๏ธ COUNT(*) vs COUNT(column) โ€” The Classic Trap!

COUNT(*) counts all rows, including those with NULLs. COUNT(column) counts only rows where that column is not NULL. This difference trips up beginners constantly. When in doubt, use COUNT(*) to count rows, and COUNT(column) only when you intentionally want to skip NULLs.

Quick Comparison

-- See all three COUNTs side by side
SELECT
    COUNT(*)               AS all_rows,
    COUNT(amount)           AS rows_with_amount,
    COUNT(DISTINCT customer) AS unique_customers
FROM orders;

Result:

all_rowsrows_with_amountunique_customers
18175

3๏ธโƒฃ SUM & AVG โ€” Totals and Averages

๐Ÿง’ ELI5 โ€” Adding Up and Sharing Equally

You and your friends each bring some candy to a party. SUM answers: "If we dump all our candy into one pile, how many pieces do we have?" AVG answers: "If we split all the candy equally among everyone, how many pieces does each person get?" SUM = the whole pile. AVG = each person's fair share.

SUM โ€” What's the Total?

-- What is the total revenue from all orders?
SELECT SUM(amount) AS total_revenue
FROM orders;

Result:

total_revenue
2738.84
-- How many total items were ordered? (sum of all quantities)
SELECT SUM(quantity) AS total_items
FROM orders;

Result:

total_items
28

AVG โ€” What's the Average?

-- What is the average order amount?
SELECT AVG(amount) AS avg_order_value
FROM orders;

Result:

avg_order_value
161.11

โš ๏ธ AVG Ignores NULLs โ€” This Can Surprise You!

The Cookbook order has NULL for amount. AVG silently skips it! It calculates 2738.84 รท 17 (not 18). This means AVG gives you the average of known values, not the average across all rows. If you want NULLs treated as 0, wrap the column: AVG(COALESCE(amount, 0)).

-- AVG with NULLs treated as 0
SELECT
    AVG(amount)                      AS avg_ignoring_nulls,
    AVG(COALESCE(amount, 0))         AS avg_treating_null_as_zero
FROM orders;

Result:

avg_ignoring_nullsavg_treating_null_as_zero
161.11152.16

See the difference? The second number is lower because it divides by 18 instead of 17.

๐Ÿ’ก Pro Tip โ€” ROUND for Clean Output

AVG often produces long decimal numbers. Wrap it with ROUND() for clean reports: ROUND(AVG(amount), 2) gives you exactly 2 decimal places. Your boss will thank you when the report doesn't show 161.108235294117647.

-- Clean, rounded averages
SELECT
    ROUND(AVG(amount), 2) AS avg_order,
    ROUND(AVG(quantity), 1) AS avg_items
FROM orders;

Result:

avg_orderavg_items
161.111.6

4๏ธโƒฃ MIN & MAX โ€” Finding Extremes

๐Ÿง’ ELI5 โ€” The Tallest and Shortest Kid

The gym teacher lines everyone up and asks: "Who is the tallest?" and "Who is the shortest?" That's MIN and MAX! They scan through all the values and pick out the absolute smallest and the absolute biggest. And here's the cool part โ€” they work on numbers, dates, and even text!

MIN & MAX on Numbers

-- Cheapest and most expensive orders
SELECT
    MIN(amount) AS cheapest_order,
    MAX(amount) AS biggest_order
FROM orders;

Result:

cheapest_orderbiggest_order
12.991200.00

MIN & MAX on Dates

Dates are compared chronologically โ€” earlier dates are "smaller."

-- When was the first and last order placed?
SELECT
    MIN(order_date) AS first_order,
    MAX(order_date) AS latest_order
FROM orders;

Result:

first_orderlatest_order
2024-01-082024-03-20

MIN & MAX on Text

Text is compared alphabetically (A comes before Z, like a dictionary).

-- Alphabetically first and last customer names
SELECT
    MIN(customer) AS first_alphabetically,
    MAX(customer) AS last_alphabetically
FROM orders;

Result:

first_alphabeticallylast_alphabetically
AliceEve

Combining All Five Aggregates in One Query

-- The complete summary dashboard
SELECT
    COUNT(*)              AS total_orders,
    SUM(amount)            AS total_revenue,
    ROUND(AVG(amount), 2) AS avg_order_value,
    MIN(amount)            AS smallest_order,
    MAX(amount)            AS largest_order
FROM orders;

Result:

total_orderstotal_revenueavg_order_valuesmallest_orderlargest_order
182738.84161.1112.991200.00

๐ŸŒ Real-Life Analogy

This is exactly what you'd see on a store manager's daily dashboard. Every morning they glance at one screen: "How many orders came in? What's total revenue? What's the average order? What's our cheapest sale? What was the biggest sale?" Five numbers. Five aggregate functions. One glance and you know how the business is doing.

5๏ธโƒฃ GROUP BY โ€” The Magic of Summarising by Category

๐Ÿง’ ELI5 โ€” Sorting M&Ms by Colour

You dump a big bag of M&Ms onto the table. Right now they're all mixed together. You separate them into piles: red pile, blue pile, green pile, yellow pile. Then you count each pile separately. "I have 12 reds, 8 blues, 15 greens, and 10 yellows." That's GROUP BY! You're splitting the data into groups and running an aggregate function on each group separately.

Without GROUP BY, aggregate functions look at all rows at once and return a single number. With GROUP BY, they look at each group independently and return one row per group.

Example 1: Orders per Category

-- How many orders in each category?
SELECT
    category,
    COUNT(*) AS order_count
FROM orders
GROUP BY category
ORDER BY order_count DESC;

Result:

categoryorder_count
Electronics7
Clothing5
Books3
Home2
Food1

PostgreSQL split the 18 rows into 5 groups (one per category), counted each pile, and gave us a nice summary.

Example 2: Revenue per Category

-- Total spending in each category
SELECT
    category,
    SUM(amount) AS total_revenue,
    ROUND(AVG(amount), 2) AS avg_order_value,
    COUNT(*) AS order_count
FROM orders
GROUP BY category
ORDER BY total_revenue DESC;

Result:

categorytotal_revenueavg_order_valueorder_count
Electronics2318.94331.287
Clothing274.9654.995
Books69.9834.993
Home60.0030.002
Food14.9914.991

Example 3: Orders per Customer

-- How much has each customer spent?
SELECT
    customer,
    COUNT(*) AS total_orders,
    SUM(amount) AS total_spent,
    ROUND(AVG(amount), 2) AS avg_order
FROM orders
GROUP BY customer
ORDER BY total_spent DESC;

Result:

customertotal_orderstotal_spentavg_order
Alice41340.98335.25
Charlie3544.98181.66
Eve3512.98256.49
Bob4219.9654.99
Diana4119.9729.99

Example 4: Orders per Month

-- Monthly order summary
SELECT
    TO_CHAR(order_date, 'YYYY-MM') AS month,
    COUNT(*) AS order_count,
    SUM(amount) AS revenue
FROM orders
GROUP BY TO_CHAR(order_date, 'YYYY-MM')
ORDER BY month;

Result:

monthorder_countrevenue
2024-0151485.96
2024-026944.94
2024-037259.96

โš ๏ธ The Golden Rule of GROUP BY

Every column in your SELECT must either be inside an aggregate function (COUNT, SUM, AVG, etc.) or listed in GROUP BY. If you write SELECT customer, product, COUNT(*) FROM orders GROUP BY customer, PostgreSQL will throw an error because product isn't aggregated or grouped. Think of it this way: if there are 4 orders for Alice, which one product should PostgreSQL show? It doesn't know โ€” so it complains.

Grouping by Multiple Columns

-- Spending per customer per category
SELECT
    customer,
    category,
    COUNT(*) AS order_count,
    SUM(amount) AS total_spent
FROM orders
GROUP BY customer, category
ORDER BY customer, total_spent DESC;

Result (first rows):

customercategoryorder_counttotal_spent
AliceElectronics21225.99
AliceClothing175.00
AliceBooks139.99
BobClothing2109.98
BobElectronics179.99
... more rows ............

๐ŸŒ Real-Life Analogy

Grouping by multiple columns is like a school report card. You don't just want "average grade across all subjects" โ€” you want it broken down by student AND subject. "Alice got 92 in Math, 88 in English. Bob got 75 in Math, 91 in English." Each unique combination of student + subject gets its own row.

6๏ธโƒฃ HAVING โ€” Filtering Groups

๐Ÿง’ ELI5 โ€” WHERE vs. HAVING

You're the teacher dividing students into study groups. WHERE is like saying "Only students taller than 5 feet can participate" โ€” you filter individual people before making groups. HAVING is like saying "After the groups are formed, only keep groups with more than 3 members" โ€” you filter entire groups after they've been created. WHERE filters rows. HAVING filters groups.

The Execution Order

1
FROM โ€” Pick the table
2
WHERE โ€” Filter individual rows
3
GROUP BY โ€” Form groups
4
HAVING โ€” Filter groups
5
SELECT โ€” Pick columns
6
ORDER BY โ€” Sort results

Example 1: Customers With 4+ Orders

-- Which customers have placed 4 or more orders?
SELECT
    customer,
    COUNT(*) AS order_count
FROM orders
GROUP BY customer
HAVING COUNT(*) >= 4
ORDER BY order_count DESC;

Result:

customerorder_count
Alice4
Bob4
Diana4

Charlie and Eve only have 3 orders each, so HAVING filtered them out.

Example 2: Categories With Over $100 in Revenue

-- Which categories generated more than $100 total?
SELECT
    category,
    SUM(amount) AS total_revenue
FROM orders
GROUP BY category
HAVING SUM(amount) > 100
ORDER BY total_revenue DESC;

Result:

categorytotal_revenue
Electronics2318.94
Clothing274.96

Books ($69.98), Home ($60.00), and Food ($14.99) all fell below the $100 threshold.

Example 3: WHERE + HAVING Together

This is where the real power lives โ€” filter rows first, then filter the resulting groups.

-- Among orders placed in 2024 Q1 (Jan-Mar),
-- which customers spent more than $200 total?
SELECT
    customer,
    SUM(amount) AS q1_spending
FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31'
GROUP BY customer
HAVING SUM(amount) > 200
ORDER BY q1_spending DESC;

Result:

customerq1_spending
Alice1340.98
Charlie544.98
Eve512.98
Bob219.96

โš ๏ธ Common Mistake โ€” Using WHERE With Aggregates!

This is wrong and will throw an error:

-- โŒ WRONG โ€” You can't use WHERE with aggregate functions!
SELECT customer, COUNT(*)
FROM orders
WHERE COUNT(*) >= 4   -- ERROR!
GROUP BY customer;

-- โœ… CORRECT โ€” Use HAVING for aggregate conditions
SELECT customer, COUNT(*)
FROM orders
GROUP BY customer
HAVING COUNT(*) >= 4;  -- Works!

Remember: WHERE runs before grouping โ€” at that point, the groups don't exist yet, so there's nothing to count. HAVING runs after grouping, when the counts are available.

7๏ธโƒฃ DISTINCT โ€” Removing Duplicates

We touched on COUNT(DISTINCT ...) earlier, but let's explore DISTINCT more fully inside aggregate functions.

-- List all unique categories
SELECT DISTINCT category
FROM orders
ORDER BY category;

Result:

category
Books
Clothing
Electronics
Food
Home

DISTINCT Inside Aggregates

-- For each category: how many unique customers ordered?
SELECT
    category,
    COUNT(*) AS total_orders,
    COUNT(DISTINCT customer) AS unique_customers
FROM orders
GROUP BY category
ORDER BY unique_customers DESC;

Result:

categorytotal_ordersunique_customers
Electronics74
Clothing53
Books33
Home22
Food11

Electronics has 7 orders but only 4 unique buyers โ€” so some customers ordered electronics more than once.

๐Ÿ’ก Pro Tip โ€” SUM(DISTINCT ...) Exists But Is Rare

You can use DISTINCT with SUM too: SUM(DISTINCT amount) would add up only unique amounts, skipping duplicates. This is rarely useful in practice, but it's good to know it exists. The most common combo by far is COUNT(DISTINCT column).

8๏ธโƒฃ Combining Everything โ€” Building Complex Queries

Now let's put all the pieces together. We'll build a real business question step by step.

The Question

"Find the top 3 customers by total spending, who have ordered more than 3 times, sorted by total amount descending."

Step 1: Start with GROUP BY to get per-customer totals

SELECT
    customer,
    COUNT(*) AS total_orders,
    SUM(amount) AS total_spent
FROM orders
GROUP BY customer;

Step 2: Add HAVING to keep only customers with 3+ orders

SELECT
    customer,
    COUNT(*) AS total_orders,
    SUM(amount) AS total_spent
FROM orders
GROUP BY customer
HAVING COUNT(*) > 3;

Step 3: Add ORDER BY and LIMIT for the final result

SELECT
    customer,
    COUNT(*) AS total_orders,
    SUM(amount) AS total_spent
FROM orders
GROUP BY customer
HAVING COUNT(*) > 3
ORDER BY total_spent DESC
LIMIT 3;

Final Result:

customertotal_orderstotal_spent
Alice41340.98
Bob4219.96
Diana4119.97

๐ŸŒ Real-Life Analogy

This is how a marketing team finds their VIP customers. They don't just want the biggest spenders โ€” they want people who are consistently buying (more than 3 orders) AND spending the most. That's how you decide who gets the loyalty discount or the thank-you email.

9๏ธโƒฃ Real-World Analytics Examples

Let's solve some questions that come up in real businesses every day. These are the kinds of queries you'll write in your job.

Monthly Revenue Report

-- Monthly revenue with running comparison
SELECT
    TO_CHAR(order_date, 'Month YYYY') AS month_name,
    COUNT(*)                             AS orders,
    SUM(quantity)                         AS items_sold,
    SUM(amount)                           AS revenue,
    ROUND(AVG(amount), 2)                AS avg_order_value
FROM orders
WHERE amount IS NOT NULL
GROUP BY TO_CHAR(order_date, 'Month YYYY'),
         DATE_TRUNC('month', order_date)
ORDER BY DATE_TRUNC('month', order_date);

Result:

month_nameordersitems_soldrevenueavg_order_value
January 2024561485.96297.19
February 202468944.94157.49
March 202469259.9643.33

Most Popular Products (by Quantity Sold)

-- Which products sell the most units?
SELECT
    product,
    SUM(quantity) AS total_units_sold,
    SUM(amount)   AS total_revenue
FROM orders
WHERE amount IS NOT NULL
GROUP BY product
ORDER BY total_units_sold DESC
LIMIT 5;

Result:

producttotal_units_soldtotal_revenue
Water Bottle415.00
T-Shirt319.99
Wireless Mouse225.99
Monitor2349.99
Phone Case212.99

Customer Lifetime Value (CLV)

-- Full customer profile: spending, frequency, date range
SELECT
    customer,
    COUNT(*) AS total_orders,
    SUM(amount) AS lifetime_value,
    ROUND(AVG(amount), 2) AS avg_order_value,
    MIN(order_date) AS first_order,
    MAX(order_date) AS last_order,
    MAX(order_date) - MIN(order_date) AS days_active,
    COUNT(DISTINCT category) AS categories_shopped
FROM orders
GROUP BY customer
ORDER BY lifetime_value DESC;

Result:

customerorderslifetime_valueavg_orderfirstlastdayscategories
Alice41340.98335.25Jan 15Mar 20653
Charlie3544.98181.66Jan 25Mar 10452
Eve3512.98256.49Feb 01Mar 15432
Bob4219.9654.99Jan 20Mar 01413
Diana4119.9729.99Jan 08Mar 12643

๐ŸŒ Real-Life Analogy

This CLV query is exactly what companies like Amazon, Netflix, and Spotify run every day. They want to know: "Which customers are most valuable? Who buys frequently? Who shops across many categories?" This single query gives you a full customer profile you could put on a dashboard.

๐Ÿ‹๏ธ Practice Exercises

Time to practice! Solve these yourself before peeking at the answers. Use the orders table from the beginning.

Exercise 1: Total Revenue Calculator

Write a query that returns the total revenue (sum of all amounts) and the total number of items sold (sum of all quantities).

Reveal Solution
SELECT
    SUM(amount) AS total_revenue,
    SUM(quantity) AS total_items_sold
FROM orders;

Returns: total_revenue = 2738.84, total_items_sold = 28

Exercise 2: Category Breakdown

For each category, show the number of orders, total revenue, and average order value (rounded to 2 decimal places). Sort by total revenue descending.

Reveal Solution
SELECT
    category,
    COUNT(*) AS order_count,
    SUM(amount) AS total_revenue,
    ROUND(AVG(amount), 2) AS avg_order_value
FROM orders
GROUP BY category
ORDER BY total_revenue DESC;

Exercise 3: Big Spenders Only

Find customers who have spent more than $500 in total. Show their name, total orders, and total spent.

Reveal Solution
SELECT
    customer,
    COUNT(*) AS total_orders,
    SUM(amount) AS total_spent
FROM orders
GROUP BY customer
HAVING SUM(amount) > 500
ORDER BY total_spent DESC;

Returns: Alice ($1340.98), Charlie ($544.98), Eve ($512.98)

Exercise 4: Unique Buyer Report

For each category, count the number of unique customers who ordered. Only include categories with at least 2 unique buyers.

Reveal Solution
SELECT
    category,
    COUNT(DISTINCT customer) AS unique_buyers
FROM orders
GROUP BY category
HAVING COUNT(DISTINCT customer) >= 2
ORDER BY unique_buyers DESC;

Returns: Electronics (4), Clothing (3), Books (3), Home (2)

Exercise 5: February Only

Show the total revenue per customer for orders placed only in February 2024. Sort by revenue descending.

Reveal Solution
SELECT
    customer,
    SUM(amount) AS feb_revenue
FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-29'
GROUP BY customer
ORDER BY feb_revenue DESC;

Exercise 6: The Full Dashboard (Challenge!)

Create a comprehensive category report showing: category name, total orders, total revenue, average order value (rounded), min order, max order, and number of unique customers. Only include categories with total revenue over $50. Sort by revenue descending.

Reveal Solution
SELECT
    category,
    COUNT(*) AS total_orders,
    SUM(amount) AS total_revenue,
    ROUND(AVG(amount), 2) AS avg_order,
    MIN(amount) AS min_order,
    MAX(amount) AS max_order,
    COUNT(DISTINCT customer) AS unique_customers
FROM orders
GROUP BY category
HAVING SUM(amount) > 50
ORDER BY total_revenue DESC;

๐Ÿ“ Quiz โ€” Test Your Knowledge!

Question 1: What's the difference between COUNT(*) and COUNT(column)?

Question 2: What does AVG do with NULL values?

Question 3: When do you use HAVING instead of WHERE?

Question 4: What happens if you SELECT a column that isn't in GROUP BY and isn't inside an aggregate?

Question 5: What is the correct SQL order of execution?

Question 6: Which query finds categories with more than 3 orders?

Question 7: What does COUNT(DISTINCT category) return for our orders table?

๐ŸŽ‰ What's Next?

5
Aggregate functions mastered
20+
SQL examples practiced
7
Quiz questions conquered

You've just unlocked one of the most important skill sets in SQL โ€” the ability to summarise data instead of just listing it. Here's what you conquered:

ConceptWhat It DoesKey Syntax
COUNTCounts rowsCOUNT(*), COUNT(col), COUNT(DISTINCT col)
SUMAdds up valuesSUM(amount)
AVGCalculates the meanROUND(AVG(amount), 2)
MIN / MAXFinds extremesMIN(date), MAX(price)
GROUP BYSplits into groupsGROUP BY category
HAVINGFilters groupsHAVING COUNT(*) > 3
DISTINCTRemoves duplicatesCOUNT(DISTINCT customer)

๐Ÿ”ฎ Coming Up Next: JOINs

So far, all our data has lived in one table. But real databases have many tables โ€” customers in one, orders in another, products in a third. In the next module you'll learn how to combine tables using JOINs. It's like connecting puzzle pieces to see the full picture. Imagine being able to say: "Show me each customer's name, their order details, AND the product reviews โ€” all from three different tables in one query." That's the power of JOINs!

Querying Data JOINs