15 SQL GROUP BY Practice Problems with Solutions

GROUP BY is one of the most essential SQL clauses for summarizing and analyzing data. This practice set covers 15 beginner-friendly problems progressing from basic GROUP BY with COUNT, through SUM and AVG, up to filtering groups with HAVING and sorting with ORDER BY. Click any problem to reveal the solution and explanation.

Part 1 — GROUP BY with COUNT (Problems 1–5)

GROUP BY groups rows that share a value into summary rows. COUNT tells you how many rows are in each group.

Problem 1 Count the number of orders placed by each customer. Return the customer ID and their order count. Table: orders (order_id, customer_id, order_date, total_usd)
Show solution ↓

Solution

SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id;

Explanation

GROUP BY customer_id groups all rows with the same customer_id together. COUNT(*) then counts how many rows are in each group, giving the total number of orders per customer. AS order_count gives the result column a readable name.

Problem 2 Count how many employees are in each department. Return the department ID and the employee count. Table: employees (employee_id, full_name, department_id, salary, hire_date)
Show solution ↓

Solution

SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id;

Explanation

GROUP BY department_id creates one group per department. COUNT(*) counts how many employee rows fall into each group. The result shows one row per department with the total number of employees in it.

Problem 3 Count the number of products in each category. Return the category and the product count. Table: products (product_id, product_name, category, price_usd, stock_quantity)
Show solution ↓

Solution

SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category;

Explanation

GROUP BY category groups all products that share the same category. COUNT(*) counts how many products are in each category group, returning one summary row per category.

Problem 4 Count how many support tickets were submitted for each issue type. Return the issue type and the ticket count. Table: support_tickets (ticket_id, customer_id, issue_type, status, submitted_date)
Show solution ↓

Solution

SELECT issue_type, COUNT(*) AS ticket_count
FROM support_tickets
GROUP BY issue_type;

Explanation

GROUP BY issue_type groups tickets by their issue category. COUNT(*) counts the total number of tickets in each group, making it easy to see which types of issues are most common.

Problem 5 Count how many rides were completed by each driver. Return the driver ID and their completed ride count. Table: rides (ride_id, driver_id, passenger_id, fare_usd, status, ride_date)
Show solution ↓

Solution

SELECT driver_id, COUNT(*) AS ride_count
FROM rides
WHERE status = 'Completed'
GROUP BY driver_id;

Explanation

WHERE status = 'Completed' filters to only completed rides before grouping. GROUP BY driver_id then groups the filtered rows by driver, and COUNT(*) counts how many completed rides each driver has. WHERE always runs before GROUP BY.

Part 2 — GROUP BY with SUM and AVG (Problems 6–10)

SUM adds up the values in a column for each group. AVG calculates the average value for each group.

Problem 6 Find the total revenue generated by each product category. Return the category and total revenue. Table: sales (sale_id, product_name, category, quantity_sold, revenue_usd, sale_date)
Show solution ↓

Solution

SELECT category, SUM(revenue_usd) AS total_revenue
FROM sales
GROUP BY category;

Explanation

GROUP BY category groups all sales by their category. SUM(revenue_usd) then adds up the revenue for every sale within each group, giving the total revenue per category.

Problem 7 Find the average salary in each department. Return the department ID and the average salary. Table: employees (employee_id, full_name, department_id, salary, hire_date)
Show solution ↓

Solution

SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;

Explanation

GROUP BY department_id groups employees by their department. AVG(salary) calculates the average salary across all employees in each group, returning one row per department.

Problem 8 Find the total amount spent by each customer. Return the customer ID and their total spend. Table: orders (order_id, customer_id, order_date, total_usd)
Show solution ↓

Solution

SELECT customer_id, SUM(total_usd) AS total_spend
FROM orders
GROUP BY customer_id;

Explanation

GROUP BY customer_id groups all orders belonging to the same customer. SUM(total_usd) adds up all the order totals for each customer, giving their lifetime spend.

Problem 9 Find the average fare for each ride distance category. Return the distance category and average fare. Table: rides (ride_id, driver_id, passenger_id, fare_usd, distance_category, ride_date)
Show solution ↓

Solution

SELECT distance_category, AVG(fare_usd) AS avg_fare
FROM rides
GROUP BY distance_category;

Explanation

GROUP BY distance_category groups rides into their distance bands. AVG(fare_usd) then calculates the average fare paid within each distance category, making it easy to compare pricing across trip lengths.

Problem 10 Find the total quantity sold and average sale price for each product. Return the product name, total quantity sold, and average price. Table: sales (sale_id, product_name, category, quantity_sold, revenue_usd, sale_date)
Show solution ↓

Solution

SELECT product_name, SUM(quantity_sold) AS total_quantity, AVG(revenue_usd) AS avg_price
FROM sales
GROUP BY product_name;

Explanation

GROUP BY product_name groups all sales for each product. SUM(quantity_sold) totals the units sold and AVG(revenue_usd) calculates the average sale value — both applied to the same groups in a single query.

Part 3 — GROUP BY with HAVING and ORDER BY (Problems 11–15)

HAVING filters groups after aggregation — like WHERE but for grouped results. ORDER BY sorts the final output.

Problem 11 Find customers who have placed more than 3 orders. Return the customer ID and their order count. Table: orders (order_id, customer_id, order_date, total_usd)
Show solution ↓

Solution

SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 3;

Explanation

GROUP BY customer_id groups orders by customer and COUNT(*) counts orders per customer. HAVING COUNT(*) > 3 then filters to only keep groups where the count exceeds 3. HAVING is used here instead of WHERE because we're filtering on an aggregated value.

Problem 12 Find product categories with total revenue greater than $10,000. Return the category and total revenue. Table: sales (sale_id, product_name, category, quantity_sold, revenue_usd, sale_date)
Show solution ↓

Solution

SELECT category, SUM(revenue_usd) AS total_revenue
FROM sales
GROUP BY category
HAVING SUM(revenue_usd) > 10000;

Explanation

SUM(revenue_usd) totals revenue per category. HAVING SUM(revenue_usd) > 10000 filters out any category whose total revenue is $10,000 or less, leaving only the high-performing categories.

Problem 13 Find departments where the average salary is above $70,000, sorted from highest to lowest average salary. Return the department ID and average salary. Table: employees (employee_id, full_name, department_id, salary, hire_date)
Show solution ↓

Solution

SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 70000
ORDER BY avg_salary DESC;

Explanation

GROUP BY department_id groups employees by department. HAVING AVG(salary) > 70000 keeps only departments whose average salary exceeds $70,000. ORDER BY avg_salary DESC then sorts those departments from the highest average salary down.

Problem 14 Find issue types that have more than 10 open support tickets, sorted by ticket count from highest to lowest. Return the issue type and open ticket count. Table: support_tickets (ticket_id, customer_id, issue_type, status, submitted_date)
Show solution ↓

Solution

SELECT issue_type, COUNT(*) AS open_ticket_count
FROM support_tickets
WHERE status = 'Open'
GROUP BY issue_type
HAVING COUNT(*) > 10
ORDER BY open_ticket_count DESC;

Explanation

WHERE status = 'Open' filters to only open tickets before grouping. GROUP BY issue_type groups the filtered rows by issue type. HAVING COUNT(*) > 10 keeps only issue types with more than 10 open tickets. ORDER BY open_ticket_count DESC sorts the results from the most common issue type down. Notice WHERE filters rows before grouping while HAVING filters groups after aggregation.

Problem 15 Find drivers who have earned more than $500 in total fares, sorted from highest to lowest earnings. Return the driver ID and total fares earned. Table: rides (ride_id, driver_id, passenger_id, fare_usd, status, ride_date)
Show solution ↓

Solution

SELECT driver_id, SUM(fare_usd) AS total_earned
FROM rides
WHERE status = 'Completed'
GROUP BY driver_id
HAVING SUM(fare_usd) > 500
ORDER BY total_earned DESC;

Explanation

WHERE status = 'Completed' ensures only completed rides count toward earnings. GROUP BY driver_id groups rides by driver. SUM(fare_usd) totals their earnings. HAVING SUM(fare_usd) > 500 keeps only drivers who earned more than $500. ORDER BY total_earned DESC ranks them from highest earner down.

Ready to practice GROUP BY interactively? Write real queries and get instant feedback in the SQL Drills interactive exercise tool.

Practice GROUP BY Interactively →

Looking for more practice? Try the HAVING exercises or head back to the SQL Drills Learn hub for more practice sets and guides.