SQLab Hub
Lesson 6 · SQL GROUP BY
Lesson 6 · Fundamentals

SQL GROUP BY: Syntax, Examples & Practice

SQL GROUP BY groups rows that have the same value and returns one result row for each group. It is commonly used with aggregate functions such as COUNT(), SUM(), AVG(), MIN() and MAX() to calculate a metric for each category.

Σ COUNT, SUM, AVG, MIN, MAX ▶ Runnable Playground examples 📊 8 analyst examples 🧪 8 practice exercises 📝 10-question quiz ⏱ ~30 min

What Is SQL GROUP BY?

Here is the question "how many employees work in each department?" written in SQL:

SQLite · runs in the Playground
SELECT department,
       COUNT(*) AS employee_count
FROM employees
GROUP BY department;

▶ Run this query in the SQL Playground

Number of employees in each department
departmentemployee_count
Customer Success15
Design10
Engineering11
Finance10
HR12
Legal16
Marketing13
Operations13
Product9
Sales11
✓ 10 rows · real output from the Playground database

The employees table has 120 rows. GROUP BY department sorts those rows into 10 piles — one per department — and COUNT(*) counts the rows in each pile. The result has one row per group instead of one row per employee: 120 rows in, 10 rows out.

Without GROUP BY vs With GROUP BY

Without GROUP BY

A list of rows

Every employee on its own line — accurate, but you cannot see the pattern.

With GROUP BY

A summary per category

One line per department with a number attached — the answer to a question.

Think of GROUP BY as making categories. Whenever a question contains "per", "each" or "by" — orders per customer, revenue by city, average salary in each department — the word after it is the column you group by, and the thing being measured is the aggregate function.

GROUP BY builds on two earlier lessons: choosing columns with SELECT and the other core SQL clauses, and filtering rows with WHERE.

SQL GROUP BY Syntax

The pattern every grouped query follows.

SQL
SELECT   column_name,                 -- what to group by
         aggregate_function(column)   -- what to calculate per group
FROM     table_name
WHERE    condition                    -- optional: filter rows first
GROUP BY column_name;
✓ The rule: every column in SELECT must either appear in GROUP BY or be inside an aggregate function.

Where GROUP BY Goes: Clause Order

SQL clauses are written in a fixed order. GROUP BY comes after FROM and WHERE, and before HAVING and ORDER BY:

SELECT FROM WHERE GROUP BY HAVING ORDER BY
What each clause does in a grouped query and when it is applied
ClausePurposeWhen it is applied
WHEREFilters individual rowsBefore grouping
GROUP BYPuts the remaining rows into groupsAfter WHERE
HAVINGFilters whole groupsAfter the aggregates are calculated
ORDER BYSorts the final rowsLast
❌ Wrong order
SQL
SELECT department
GROUP BY department
FROM employees;
✗ Syntax error — FROM must come before GROUP BY
✅ Correct order
SQL
SELECT department
FROM employees
GROUP BY department;
✓ FROM, then GROUP BY

GROUP BY with Aggregate Functions

GROUP BY makes the groups; an aggregate function turns each group into a number.

An aggregate function takes many rows and returns one value. With GROUP BY it runs once per group. These five cover almost every report — select one to jump to its example:

GROUP BY with COUNT()

COUNT(*) returns the number of rows in each group. How many orders are in each status?

SQLite · runs in the Playground
SELECT status,
       COUNT(*) AS orders
FROM orders
GROUP BY status;

▶ Run this query in the SQL Playground

Number of orders per status
statusorders
cancelled27
delivered82
pending23
processing28
refunded19
shipped21
✓ 6 rows · real output from the Playground database

COUNT(column) is slightly different: it skips rows where that column is NULL. Only shipped and delivered orders have a shipped_date, so the two counts disagree:

SQLite · runs in the Playground
SELECT status,
       COUNT(*)            AS all_orders,
       COUNT(shipped_date) AS with_ship_date
FROM orders
GROUP BY status;

▶ Run this query in the SQL Playground

COUNT(*) compared with COUNT(column) per order status
statusall_orderswith_ship_date
cancelled270
delivered8282
pending230
processing280
refunded190
shipped2121
✓ 6 rows · real output from the Playground database

GROUP BY with SUM()

SUM(column) adds up a numeric column inside each group. What is the total payroll of each department?

SQLite · runs in the Playground
SELECT department,
       SUM(salary) AS total_salary
FROM employees
GROUP BY department;

▶ Run this query in the SQL Playground

Total salary per department
departmenttotal_salary
Customer Success2012099
Design1339649
Engineering1441308
Finance1214710
HR1489813
✓ 10 rows — first 5 shown · real output from the Playground database

GROUP BY with AVG()

AVG(column) returns the mean of each group. What is the average product price in each category?

SQLite · runs in the Playground
SELECT category,
       ROUND(AVG(price), 2) AS avg_price
FROM products
GROUP BY category;

▶ Run this query in the SQL Playground

Average product price per category
categoryavg_price
Automotive1161.09
Books995.44
Clothing1202.85
Electronics752.67
Food & Beverage1249.46
✓ 10 rows — first 5 shown · real output from the Playground database

ROUND(…, 2) only tidies the output to two decimals. AVG ignores NULL values rather than treating them as zero.

GROUP BY with MIN() and MAX()

MIN and MAX return the smallest and largest value in each group. One query can use several aggregates at once — what is the salary range in each department?

SQLite · runs in the Playground
SELECT department,
       COUNT(*)    AS employees,
       MIN(salary) AS lowest,
       MAX(salary) AS highest
FROM employees
GROUP BY department;

▶ Run this query in the SQL Playground

Lowest and highest salary per department
departmentemployeeslowesthighest
Customer Success1561774203999
Design1046044194543
Engineering1151465199029
Finance1050463196032
HR1250360191037
✓ 10 rows — first 5 shown · real output from the Playground database

MIN and MAX also work on text and dates: MIN(order_date) is each group's earliest order. The full set of functions is covered in SQL aggregate functions.

GROUP BY Multiple Columns

One group for every unique combination.

List more than one column and GROUP BY creates a group for each combination of their values. Grouping employees by department, status counts active and on-leave staff separately inside every department:

SQLite · runs in the Playground
SELECT department, status,
       COUNT(*) AS employees
FROM employees
GROUP BY department, status
ORDER BY department, status;

▶ Run this query in the SQL Playground

Employees per department and status
departmentstatusemployees
Customer Successactive13
Customer Successon_leave2
Designactive9
Designon_leave1
Engineeringactive11
Financeactive10
HRactive9
HRon_leave3
✓ 16 rows — first 8 shown · real output from the Playground database

Customer Success appears twice — once for active and once for on_leave — because those are two different combinations. Engineering appears once, because nobody there is on leave: a combination with no rows produces no group. A location report works the same way: GROUP BY country, city keeps a Springfield in one country apart from a Springfield in another.

⚠ Each column you add splits the groups further. Add too many and every group shrinks to a single row, which is no longer a summary. Subtotals and grand totals across several columns are covered in advanced GROUP BY with ROLLUP and CUBE.

WHERE with GROUP BY

Choose which rows are allowed into the groups.

WHERE runs before grouping, so it decides which rows get counted at all. How many delivered orders went to each city?

SQLite · runs in the Playground
SELECT shipping_city,
       COUNT(*) AS delivered_orders
FROM orders
WHERE status = 'delivered'
GROUP BY shipping_city
ORDER BY delivered_orders DESC
LIMIT 5;

▶ Run this query in the SQL Playground

Delivered orders per shipping city, top five
shipping_citydelivered_orders
Jacksonville9
Los Angeles7
Chicago6
Phoenix5
Indianapolis5
✓ 5 rows · real output from the Playground database

Of 200 orders, WHERE keeps the 82 delivered ones; only those are grouped by city. Cancelled, pending and refunded orders never reach COUNT. For the operators WHERE supports, see how SQL WHERE filters rows.

GROUP BY with HAVING

WHERE filters rows before grouping. HAVING filters groups after aggregation.

Which cities received 12 or more orders? The condition is about each city's count — a number that does not exist until the groups have been built. That is what HAVING is for:

SQLite · runs in the Playground
SELECT shipping_city,
       COUNT(*) AS order_count
FROM orders
GROUP BY shipping_city
HAVING COUNT(*) >= 12
ORDER BY order_count DESC;

▶ Run this query in the SQL Playground

Cities with 12 or more orders
shipping_cityorder_count
Jacksonville15
Los Angeles14
New York12
Columbus12
✓ 4 rows · real output from the Playground database

There are 20 cities; HAVING keeps the 4 whose group has at least 12 rows.

Why WHERE Cannot Replace HAVING Here

❌ WHERE cannot use COUNT
SQL
SELECT shipping_city, COUNT(*)
FROM orders
WHERE COUNT(*) >= 12
GROUP BY shipping_city;
✗ Error in every database, SQLite included: "misuse of aggregate: COUNT()"
✅ HAVING runs after grouping
SQL
SELECT shipping_city, COUNT(*)
FROM orders
GROUP BY shipping_city
HAVING COUNT(*) >= 12;
✓ The count exists by the time HAVING is checked

WHERE looks at one row at a time, and a single row has no count. HAVING looks at one group at a time.

Using WHERE and HAVING Together

They are not alternatives — a query can use both. Among delivered orders only, which cities have 5 or more?

SQLite · runs in the Playground
SELECT shipping_city,
       COUNT(*) AS delivered_orders
FROM orders
WHERE status = 'delivered'     -- 1. filter rows
GROUP BY shipping_city         -- 2. build groups
HAVING COUNT(*) >= 5           -- 3. filter groups
ORDER BY delivered_orders DESC;

▶ Run this query in the SQL Playground

Cities with five or more delivered orders
shipping_citydelivered_orders
Jacksonville9
Los Angeles7
Chicago6
Phoenix5
Indianapolis5
Denver5
Dallas5
✓ 7 rows · real output from the Playground database
✓ Rule of thumb: a condition on a column goes in WHERE; a condition on an aggregate goes in HAVING. The dedicated lesson goes deeper — learn SQL HAVING for filtering grouped results.

GROUP BY with ORDER BY

GROUP BY creates the groups; ORDER BY decides the order you see them in.

Which cities bring in the most revenue? Group, total, then sort by the total:

SQLite · runs in the Playground
SELECT shipping_city,
       ROUND(SUM(total), 2) AS total_revenue
FROM orders
GROUP BY shipping_city
ORDER BY total_revenue DESC
LIMIT 5;

▶ Run this query in the SQL Playground

Top five cities by revenue
shipping_citytotal_revenue
Jacksonville36302.64
Los Angeles35977.73
Columbus35480.28
New York31667.49
Seattle31265.78
✓ 5 rows · real output from the Playground database

ORDER BY can sort by the grouped column (ORDER BY shipping_city) or by an aggregate. Because it runs last, it can use the alias total_revenue defined in SELECT.

⚠ GROUP BY does not sort. Results often look ordered by the grouped column, but no database guarantees it — if the order matters, write ORDER BY. More in the SQL ORDER BY lesson.

GROUP BY with JOIN

Aggregate across related tables.

Often the column you want to group by lives in a different table from the rows you want to count. Join first, then group the joined rows. How many orders were placed by customers living in each city? The city is in users; the orders are in orders:

SQLite · runs in the Playground
SELECT u.city,
       COUNT(o.id) AS total_orders
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.city
ORDER BY total_orders DESC
LIMIT 5;

▶ Run this query in the SQL Playground

Orders by customer home city, top five
citytotal_orders
Indianapolis24
Phoenix18
Columbus17
San Antonio13
Houston13
✓ 5 rows · real output from the Playground database

The JOIN produces one row per order with its customer's details attached; GROUP BY then collapses those rows by u.city. Prefix columns with the table alias (u., o.) so it is clear which table each comes from.

Customers with no orders: an inner JOIN drops them, so they never form a group. To report them with a count of 0, use a LEFT JOIN and count a column from the orders table — COUNT(o.id), not COUNT(*). Start with SQL JOINs if joins are new to you.

GROUP BY vs DISTINCT

Same rows, different jobs.

DISTINCT removes duplicate rows from the result. GROUP BY builds groups so that something can be calculated for each one. Used with no aggregate, they return the same rows:

DISTINCT — unique values
SQLite · runs in the Playground
SELECT DISTINCT department
FROM employees;
GROUP BY — same 10 rows
SQLite · runs in the Playground
SELECT department
FROM employees
GROUP BY department;
SELECT DISTINCT compared with GROUP BY
SELECT DISTINCTGROUP BY
PurposeRemove duplicate rowsCreate groups to summarise
Aggregates per valueNot possibleCOUNT, SUM, AVG, MIN, MAX
Filter the resultWHERE onlyWHERE and HAVING
Use it whenYou only need the unique valuesYou need a number per value

The moment you want "…and how many of each", DISTINCT cannot help and GROUP BY can. For de-duplication on its own, see the SELECT DISTINCT lesson.

Common GROUP BY Mistakes

Eight errors that account for most GROUP BY trouble — and the fix for each.

1. Selecting a column that is not grouped or aggregated

❌ first_name is neither
SQL
SELECT department, first_name, COUNT(*)
FROM employees
GROUP BY department;
✅ Only grouped columns and aggregates
SQL
SELECT department, COUNT(*)
FROM employees
GROUP BY department;

A department has many employees, so there is no single first_name to show for the group. PostgreSQL, SQL Server and MySQL (in its default ONLY_FULL_GROUP_BY mode) reject the query with an error such as "column must appear in the GROUP BY clause or be used in an aggregate function".

SQLite is lenient — and the Playground runs SQLite. There the wrong query above does not fail. It returns one arbitrary employee's name per department:
SQLite returning an arbitrary first_name for each department
departmentfirst_nameCOUNT(*)
Customer SuccessWilliam15
DesignJason10
EngineeringDaniel11
✓ 3 rows · real output from the Playground database
William is not "the" Customer Success employee — he is just a row SQLite happened to pick. Do not rely on this: the rule is the one to learn, because every other major database enforces it.

2. Using WHERE instead of HAVING

WHERE COUNT(*) > 10 fails in every database, because WHERE runs before the groups exist. Conditions on aggregates go in HAVING — see GROUP BY with HAVING above.

3. Forgetting GROUP BY

❌ Aggregate with no groups
SQL
SELECT department, COUNT(*)
FROM employees;
✅ Say what to group by
SQL
SELECT department, COUNT(*)
FROM employees
GROUP BY department;

Without GROUP BY an aggregate treats the whole table as one group. Strict databases raise the same error as mistake 1; SQLite returns a single row with the total (120) next to one arbitrary department.

4. Grouping by the wrong column

Grouping by a label that is not unique merges things that should stay apart. In the Playground, two different products are both named "Ergonomic Desk Mat XL"; GROUP BY p.name adds their sales together, while GROUP BY p.id, p.name keeps them separate. Group by the key that identifies the thing, and add the label for display.

5. Grouping by too many columns

Every column in GROUP BY splits the groups further. GROUP BY id on a table's primary key gives one group per row — each "summary" then describes a single row and every COUNT(*) is 1. If the result has as many rows as the table, a column in GROUP BY is too specific.

6. Assuming GROUP BY sorts the results

It does not. Add ORDER BY whenever the order matters, especially before LIMIT: "top 5" without ORDER BY is just "some 5".

7. Confusing GROUP BY with DISTINCT

SELECT DISTINCT department, COUNT(*) FROM employees does not count per department — DISTINCT only removes duplicate result rows. Per-group numbers need GROUP BY.

8. Assuming every database behaves the same

GROUP BY behaviour that differs between SQLite, MySQL, PostgreSQL and SQL Server
BehaviourSQLiteMySQLPostgreSQLSQL Server
Non-grouped column in SELECTAllowed (arbitrary value)Error by defaultErrorError
Column alias in GROUP BYAllowedAllowedAllowedNot allowed
Column alias in HAVINGAllowedAllowedNot allowedNot allowed
GROUP BY 1 (column position)AllowedAllowedAllowedNot allowed

Repeating the full expression — GROUP BY strftime('%Y', order_date), HAVING COUNT(*) > 5 — instead of an alias or a position works everywhere, which is why the examples in this lesson are written that way.

SQL GROUP BY Examples for Data Analysts

Eight questions an analyst gets asked, answered on the Playground's e-commerce data.

Each example gives the business question, the query, its real output and the idea to take away. All of them run in the Playground — open one, then change a column or a threshold and run it again.

Example 1 How many orders does each customer have?

Sales wants its most frequent buyers, with what each has spent.

SQLite · runs in the Playground
SELECT u.first_name, u.last_name,
       COUNT(o.id)            AS order_count,
       ROUND(SUM(o.total), 2) AS total_spent
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.first_name, u.last_name
ORDER BY order_count DESC, total_spent DESC
LIMIT 5;

▶ Run this query in the SQL Playground

How many orders does each customer have?
first_namelast_nameorder_counttotal_spent
JosephKing514593.17
AndrewWright56432.94
PaulYoung415384.26
MichaelRodriguez412059.32
LauraMartin411780.53
✓ 5 rows · real output from the Playground database

Grouping by u.id as well as the name keeps two customers who share a name in separate groups.

Key concept: Group by the key, not only the label

Example 2 What is total revenue by product category?

Merchandising wants to know which categories bring in the most money.

SQLite · runs in the Playground
SELECT p.category,
       COUNT(*)                   AS line_items,
       ROUND(SUM(oi.subtotal), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY p.category
ORDER BY revenue DESC;

▶ Run this query in the SQL Playground

What is total revenue by product category?
categoryline_itemsrevenue
Office Supplies6247482.77
Toys5844782.9
Automotive5143055.93
Food & Beverage6042452.58
Books6140552.7
✓ 10 rows — first 5 shown · real output from the Playground database

Revenue lives in order_items and the category in products, so the tables are joined first and the joined rows are grouped.

Key concept: JOIN, then GROUP BY

Example 3 What is the average order value for each order status?

Finance asks whether refunded or cancelled orders tend to be larger than delivered ones.

SQLite · runs in the Playground
SELECT status,
       COUNT(*)             AS orders,
       ROUND(AVG(total), 2) AS avg_order_value
FROM orders
GROUP BY status
ORDER BY avg_order_value DESC;

▶ Run this query in the SQL Playground

What is the average order value for each order status?
statusordersavg_order_value
shipped212841.56
refunded192626.22
delivered822570.44
processing282516.91
cancelled272447.19
pending232351.12
✓ 6 rows · real output from the Playground database

Showing COUNT(*) next to the average tells you how many orders each average is based on — an average over 19 rows deserves less trust than one over 82.

Key concept: Pair AVG with COUNT

Example 4 Which products generated the most revenue?

The classic "top N" report.

SQLite · runs in the Playground
SELECT p.id, p.name,
       SUM(oi.quantity)           AS units_sold,
       ROUND(SUM(oi.subtotal), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY p.id, p.name
ORDER BY revenue DESC
LIMIT 5;

▶ Run this query in the SQL Playground

Which products generated the most revenue?
idnameunits_soldrevenue
119Merino Wool Sweater308995.83
38LEGO Creator Set 742pcs378530.61
60Leather Steering Wheel Cover257519.96
32Ergonomic Desk Mat XL337066.04
51Ergonomic Desk Mat XL246944.33
✓ 5 rows · real output from the Playground database

Look at the names: "Ergonomic Desk Mat XL" appears twice with different ids. They are two separate products, and grouping by p.name alone would have silently merged them. LIMIT keeps the top five after sorting.

Key concept: Top N = GROUP BY + ORDER BY + LIMIT

Example 5 How many orders were placed each month?

A monthly trend for 2024.

SQLite · runs in the Playground
SELECT strftime('%Y-%m', order_date) AS month,
       COUNT(*)             AS orders,
       ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
GROUP BY strftime('%Y-%m', order_date)
ORDER BY month;

▶ Run this query in the SQL Playground

How many orders were placed each month?
monthordersrevenue
2024-01816318.57
2024-02816998.61
2024-03616549.31
2024-04922604.85
2024-0537590.89
2024-0625186.49
✓ 12 rows — first 6 shown · real output from the Playground database

You can group by an expression, not only a column. strftime('%Y-%m', …) is SQLite's way to label each date with its year and month — see SQL date functions for the MySQL, PostgreSQL and SQL Server spellings.

Key concept: Group by an expression

Example 6 Which customers have placed four or more orders?

Marketing wants a list of repeat buyers for a loyalty offer.

SQLite · runs in the Playground
SELECT user_id,
       COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 4
ORDER BY order_count DESC;

▶ Run this query in the SQL Playground

Which customers have placed four or more orders?
user_idorder_count
775
375
1244
1184
834
544
✓ 6 rows · real output from the Playground database

The condition is about each customer's count, which only exists after grouping — so it belongs in HAVING, not WHERE. Only 6 of 114 customers qualify.

Key concept: Filter groups with HAVING

Example 7 Which payment methods collect the most money?

Only successful payments should count.

SQLite · runs in the Playground
SELECT method,
       COUNT(*)              AS payments,
       ROUND(SUM(amount), 2) AS collected
FROM payments
WHERE status = 'success'
GROUP BY method
ORDER BY collected DESC;

▶ Run this query in the SQL Playground

Which payment methods collect the most money?
methodpaymentscollected
Cryptocurrency2428333.72
Debit Card2124174.39
Bank Transfer2322173.03
Apple Pay1615593.63
PayPal1814884.42
✓ 8 rows — first 5 shown · real output from the Playground database

WHERE removes failed, pending and refunded payments before the groups are built, so they never reach SUM.

Key concept: Filter rows first with WHERE

Example 8 Which categories have revenue above 40,000?

A threshold report: keep only the categories that pass a revenue target.

SQLite · runs in the Playground
SELECT p.category,
       ROUND(SUM(oi.subtotal), 2) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY p.category
HAVING SUM(oi.subtotal) > 40000
ORDER BY revenue DESC;

▶ Run this query in the SQL Playground

Which categories have revenue above 40,000?
categoryrevenue
Office Supplies47482.77
Toys44782.9
Automotive43055.93
Food & Beverage42452.58
Books40552.7
✓ 5 rows · real output from the Playground database

HAVING can test any aggregate — here SUM — not just COUNT.

Key concept: HAVING with SUM

Practice GROUP BY Queries

8 exercises, easy to challenging, on the real Playground tables.

Write each query yourself first. Every solution has been run against the Playground database, and its explanation tells you what result to expect.

  1. 1Easy How many products are there in each category? Table: products

    Show solution
    SQLite · runs in the Playground
    SELECT category, COUNT(*) AS products
    FROM products
    GROUP BY category
    ORDER BY products DESC;

    ▶ Run this query in the SQL Playground

    One row per category — 10 in total. The largest is Food & Beverage with 17 products.

  2. 2Easy How many employees work in each city? Show the five largest offices. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT city, COUNT(*) AS employees
    FROM employees
    GROUP BY city
    ORDER BY employees DESC
    LIMIT 5;

    ▶ Run this query in the SQL Playground

    GROUP BY builds one group per city, ORDER BY sorts by size and LIMIT keeps five. San Francisco is first with 10 employees.

  3. 3Medium What is the average salary in each department, highest first? Round to whole numbers. Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT department, ROUND(AVG(salary)) AS avg_salary
    FROM employees
    GROUP BY department
    ORDER BY avg_salary DESC;

    ▶ Run this query in the SQL Playground

    AVG is calculated separately inside each department. Customer Success has the highest average (134140).

  4. 4Medium For active products only, show the total units in stock per category. Table: products

    Show solution
    SQLite · runs in the Playground
    SELECT category, SUM(stock) AS units_in_stock
    FROM products
    WHERE status = 'active'
    GROUP BY category
    ORDER BY units_in_stock DESC;

    ▶ Run this query in the SQL Playground

    WHERE filters to active products before grouping, then SUM adds each category's stock. Food & Beverage holds the most (3956 units).

  5. 5Medium Count orders for every combination of year and status. Table: orders

    Show solution
    SQLite · runs in the Playground
    SELECT strftime('%Y', order_date) AS year,
           status,
           COUNT(*) AS orders
    FROM orders
    GROUP BY strftime('%Y', order_date), status
    ORDER BY year, status;

    ▶ Run this query in the SQL Playground

    Two grouping expressions create one group per year–status pair: 18 rows. The first is 2022 / cancelled with 7 orders.

  6. 6Medium Which products have at least four reviews? Show the review count and average rating. Table: reviews

    Show solution
    SQLite · runs in the Playground
    SELECT product_id,
           COUNT(*)              AS reviews,
           ROUND(AVG(rating), 2) AS avg_rating
    FROM reviews
    GROUP BY product_id
    HAVING COUNT(*) >= 4
    ORDER BY reviews DESC, avg_rating DESC;

    ▶ Run this query in the SQL Playground

    HAVING keeps only products whose group has four or more rows — 6 products. Product 17 leads with 6 reviews.

  7. 7Challenging Which departments have an average salary above 125,000, and how many people work in each? Table: employees

    Show solution
    SQLite · runs in the Playground
    SELECT department,
           COUNT(*)           AS employees,
           ROUND(AVG(salary)) AS avg_salary
    FROM employees
    GROUP BY department
    HAVING AVG(salary) > 125000
    ORDER BY avg_salary DESC;

    ▶ Run this query in the SQL Playground

    The condition is on an aggregate, so it must be HAVING. 4 of the 10 departments qualify; Customer Success is highest at 134140.

  8. 8Challenging Find the five customers who spent the most on delivered orders. Show their name, number of delivered orders and total spent. Table: users + orders

    Show solution
    SQLite · runs in the Playground
    SELECT u.first_name, u.last_name,
           COUNT(o.id)            AS delivered_orders,
           ROUND(SUM(o.total), 2) AS total_spent
    FROM users u
    JOIN orders o ON o.user_id = u.id
    WHERE o.status = 'delivered'
    GROUP BY u.id, u.first_name, u.last_name
    ORDER BY total_spent DESC
    LIMIT 5;

    ▶ Run this query in the SQL Playground

    JOIN connects customers to orders, WHERE keeps delivered orders, GROUP BY builds one group per customer, and ORDER BY with LIMIT picks the top five. Maria Johnson is first with 9759.23.

📝 Quiz — GROUP BY & Aggregation

Test your understanding · 0/10 answered

Question 1 of 10

What does SQL GROUP BY do?

Question 2 of 10

Which query correctly counts how many students are in each class?

Question 3 of 10

What is the correct clause order that includes GROUP BY?

Question 4 of 10

In PostgreSQL, SQL Server or MySQL, why is this query rejected? SELECT rating, COUNT(film_id) FROM films;

Question 5 of 10

Which aggregate function gives the total sales amount per city?

Question 6 of 10

What does GROUP BY class, section produce?

Question 7 of 10

You want only the cities that have more than 10 orders. Which clause holds that condition?

Question 8 of 10

Does GROUP BY guarantee that the result is sorted by the grouped column?

Question 9 of 10

What is the difference between COUNT(*) and COUNT(shipped_date) in a grouped query?

Question 10 of 10

You need the list of unique departments and nothing else. Which is the clearest query?

SQL GROUP BY Interview Questions

Six questions that come up often, with concise answers.

Q1. What is the difference between GROUP BY and ORDER BY in SQL?

GROUP BY collapses rows that share a value into one summary row per group, so it changes the shape of the result. ORDER BY only changes the order of the rows. GROUP BY is written before HAVING; ORDER BY comes last.

Q2. Can you use GROUP BY without an aggregate function?

Yes. Without an aggregate, GROUP BY returns one row per unique value, the same rows as SELECT DISTINCT. It is uncommon in practice — the point of GROUP BY is to pair it with COUNT(), SUM(), AVG(), MIN() or MAX().

Q3. What happens when you SELECT a column that is not in GROUP BY?

In PostgreSQL, SQL Server and MySQL (with its default ONLY_FULL_GROUP_BY mode) the query is rejected, because the database cannot decide which row's value to show for the group. SQLite accepts the query and returns the value from an arbitrary row of each group, which is rarely what you want. The safe rule in every database: each selected column is either in GROUP BY or inside an aggregate.

Q4. What is the difference between COUNT(*) and COUNT(column) in GROUP BY?

COUNT(*) counts every row in the group, including rows where columns are NULL. COUNT(column_name) counts only rows where that column is not NULL. Use COUNT(*) for the size of the group and COUNT(column) to count the rows that actually have a value.

Q5. How does GROUP BY handle NULL values?

All rows where the grouping column is NULL are placed together in a single group. That NULL group appears in the result like any other. COUNT(*) counts its rows, while COUNT(column) on a NULL column returns 0 for it.

Q6. Write a SQL query to find the top 3 cities by total sales amount.

SELECT shipping_city, SUM(total) AS total_sales FROM orders GROUP BY shipping_city ORDER BY total_sales DESC LIMIT 3; Group by city, total each group with SUM(), sort the groups from highest to lowest, then keep three. SQL Server writes SELECT TOP 3 … instead of LIMIT.

More practice across every topic: SQL interview questions and answers.

SQL GROUP BY Cheat Sheet

Eight patterns for quick reference.

GROUP BY patterns for quick reference
PatternSQL
Count rows per groupSELECT col, COUNT(*) FROM t GROUP BY col;
Sum values per groupSELECT col, SUM(amount) FROM t GROUP BY col;
Average per groupSELECT col, AVG(score) FROM t GROUP BY col;
Min and max per groupSELECT col, MIN(val), MAX(val) FROM t GROUP BY col;
Group by multiple columnsSELECT c1, c2, COUNT(*) FROM t GROUP BY c1, c2;
Filter rows, then groupSELECT col, COUNT(*) FROM t WHERE status = 'active' GROUP BY col;
Filter groups (HAVING)SELECT col, COUNT(*) FROM t GROUP BY col HAVING COUNT(*) > 5;
Sort groups (ORDER BY)SELECT col, SUM(amt) AS total FROM t GROUP BY col ORDER BY total DESC;

Complete GROUP BY Syntax Reference

SQL
SELECT   column_name,             -- dimension: what to group by
         aggregate_fn(column)     -- metric: what to calculate
FROM     table_name               -- data source
WHERE    condition                -- filter individual rows first
GROUP BY column_name              -- define the groups
HAVING   aggregate_fn(column) > 0 -- filter groups after aggregation
ORDER BY column_name;             -- sort the final output

Frequently Asked Questions

What does GROUP BY do in SQL?

SQL GROUP BY groups rows that have the same value in one or more columns and returns one result row for each group. It is commonly used with aggregate functions such as COUNT(), SUM(), AVG(), MIN() and MAX() to calculate a value per group.

What is the syntax of GROUP BY?

The basic syntax is SELECT column_name, aggregate_function(column) FROM table_name WHERE condition GROUP BY column_name; GROUP BY is written after FROM and WHERE, and before HAVING and ORDER BY.

Can GROUP BY be used without aggregate functions?

Yes. Without an aggregate function, GROUP BY returns one row per unique value, the same rows as SELECT DISTINCT. It is normally used with an aggregate, because that is what turns the groups into useful numbers.

What is the difference between WHERE and HAVING?

WHERE filters individual rows before they are grouped. HAVING filters whole groups after aggregation. Aggregate functions such as COUNT() cannot be used in WHERE, so a condition like "more than 10 orders" belongs in HAVING.

Can GROUP BY contain multiple columns?

Yes. GROUP BY department, status creates one group for each unique combination of department and status, and each combination gets its own result row.

Does GROUP BY sort results?

No. GROUP BY does not guarantee any order, even though results often look sorted. Add ORDER BY after GROUP BY whenever the order matters.

What aggregate functions can be used with GROUP BY?

The five most common are COUNT() to count rows, SUM() to total values, AVG() for the average, MIN() for the smallest value and MAX() for the largest. Each returns one value per group.

What is the difference between GROUP BY and DISTINCT?

DISTINCT removes duplicate rows from the result. GROUP BY builds groups so that an aggregate can be calculated for each one. Use DISTINCT when you only need unique values and GROUP BY when you need a count, total or average per value.

Why do I get a GROUP BY error?

The most common cause is selecting a column that is neither listed in GROUP BY nor wrapped in an aggregate function. PostgreSQL, SQL Server and MySQL reject such a query. Add the column to GROUP BY, wrap it in an aggregate, or remove it from SELECT.

How can I practice GROUP BY queries?

Run the examples from this lesson in the free SQL Playground, which has orders, products, employees and users tables, then try the GROUP BY challenges for graded problems.