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.
What Is SQL GROUP BY?
Here is the question "how many employees work in each department?" written in SQL:
SELECT department,
COUNT(*) AS employee_count
FROM employees
GROUP BY department;
▶ Run this query in the SQL Playground
| department | employee_count |
|---|---|
| Customer Success | 15 |
| Design | 10 |
| Engineering | 11 |
| Finance | 10 |
| HR | 12 |
| Legal | 16 |
| Marketing | 13 |
| Operations | 13 |
| Product | 9 |
| Sales | 11 |
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
A list of rows
Every employee on its own line — accurate, but you cannot see the pattern.
A summary per category
One line per department with a number attached — the answer to a question.
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.
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;
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:
| Clause | Purpose | When it is applied |
|---|---|---|
| WHERE | Filters individual rows | Before grouping |
| GROUP BY | Puts the remaining rows into groups | After WHERE |
| HAVING | Filters whole groups | After the aggregates are calculated |
| ORDER BY | Sorts the final rows | Last |
SELECT department
GROUP BY department
FROM employees;
SELECT department
FROM employees
GROUP BY department;
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?
SELECT status,
COUNT(*) AS orders
FROM orders
GROUP BY status;
▶ Run this query in the SQL Playground
| status | orders |
|---|---|
| cancelled | 27 |
| delivered | 82 |
| pending | 23 |
| processing | 28 |
| refunded | 19 |
| shipped | 21 |
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:
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
| status | all_orders | with_ship_date |
|---|---|---|
| cancelled | 27 | 0 |
| delivered | 82 | 82 |
| pending | 23 | 0 |
| processing | 28 | 0 |
| refunded | 19 | 0 |
| shipped | 21 | 21 |
GROUP BY with SUM()
SUM(column) adds up a numeric column inside each group. What is the total payroll of each department?
SELECT department,
SUM(salary) AS total_salary
FROM employees
GROUP BY department;
▶ Run this query in the SQL Playground
| department | total_salary |
|---|---|
| Customer Success | 2012099 |
| Design | 1339649 |
| Engineering | 1441308 |
| Finance | 1214710 |
| HR | 1489813 |
GROUP BY with AVG()
AVG(column) returns the mean of each group. What is the average product price in each category?
SELECT category,
ROUND(AVG(price), 2) AS avg_price
FROM products
GROUP BY category;
▶ Run this query in the SQL Playground
| category | avg_price |
|---|---|
| Automotive | 1161.09 |
| Books | 995.44 |
| Clothing | 1202.85 |
| Electronics | 752.67 |
| Food & Beverage | 1249.46 |
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?
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
| department | employees | lowest | highest |
|---|---|---|---|
| Customer Success | 15 | 61774 | 203999 |
| Design | 10 | 46044 | 194543 |
| Engineering | 11 | 51465 | 199029 |
| Finance | 10 | 50463 | 196032 |
| HR | 12 | 50360 | 191037 |
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:
SELECT department, status,
COUNT(*) AS employees
FROM employees
GROUP BY department, status
ORDER BY department, status;
▶ Run this query in the SQL Playground
| department | status | employees |
|---|---|---|
| Customer Success | active | 13 |
| Customer Success | on_leave | 2 |
| Design | active | 9 |
| Design | on_leave | 1 |
| Engineering | active | 11 |
| Finance | active | 10 |
| HR | active | 9 |
| HR | on_leave | 3 |
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.
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?
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
| shipping_city | delivered_orders |
|---|---|
| Jacksonville | 9 |
| Los Angeles | 7 |
| Chicago | 6 |
| Phoenix | 5 |
| Indianapolis | 5 |
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:
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
| shipping_city | order_count |
|---|---|
| Jacksonville | 15 |
| Los Angeles | 14 |
| New York | 12 |
| Columbus | 12 |
There are 20 cities; HAVING keeps the 4 whose group has at least 12 rows.
Why WHERE Cannot Replace HAVING Here
SELECT shipping_city, COUNT(*)
FROM orders
WHERE COUNT(*) >= 12
GROUP BY shipping_city;
SELECT shipping_city, COUNT(*)
FROM orders
GROUP BY shipping_city
HAVING COUNT(*) >= 12;
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?
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
| shipping_city | delivered_orders |
|---|---|
| Jacksonville | 9 |
| Los Angeles | 7 |
| Chicago | 6 |
| Phoenix | 5 |
| Indianapolis | 5 |
| Denver | 5 |
| Dallas | 5 |
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:
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
| shipping_city | total_revenue |
|---|---|
| Jacksonville | 36302.64 |
| Los Angeles | 35977.73 |
| Columbus | 35480.28 |
| New York | 31667.49 |
| Seattle | 31265.78 |
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 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:
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
| city | total_orders |
|---|---|
| Indianapolis | 24 |
| Phoenix | 18 |
| Columbus | 17 |
| San Antonio | 13 |
| Houston | 13 |
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.
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:
SELECT DISTINCT department
FROM employees;
SELECT department
FROM employees
GROUP BY department;
| SELECT DISTINCT | GROUP BY | |
|---|---|---|
| Purpose | Remove duplicate rows | Create groups to summarise |
| Aggregates per value | Not possible | COUNT, SUM, AVG, MIN, MAX |
| Filter the result | WHERE only | WHERE and HAVING |
| Use it when | You only need the unique values | You 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
SELECT department, first_name, COUNT(*)
FROM employees
GROUP BY department;
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".
| department | first_name | COUNT(*) |
|---|---|---|
| Customer Success | William | 15 |
| Design | Jason | 10 |
| Engineering | Daniel | 11 |
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
SELECT department, COUNT(*)
FROM employees;
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
| Behaviour | SQLite | MySQL | PostgreSQL | SQL Server |
|---|---|---|---|---|
| Non-grouped column in SELECT | Allowed (arbitrary value) | Error by default | Error | Error |
| Column alias in GROUP BY | Allowed | Allowed | Allowed | Not allowed |
| Column alias in HAVING | Allowed | Allowed | Not allowed | Not allowed |
| GROUP BY 1 (column position) | Allowed | Allowed | Allowed | Not 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.
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
| first_name | last_name | order_count | total_spent |
|---|---|---|---|
| Joseph | King | 5 | 14593.17 |
| Andrew | Wright | 5 | 6432.94 |
| Paul | Young | 4 | 15384.26 |
| Michael | Rodriguez | 4 | 12059.32 |
| Laura | Martin | 4 | 11780.53 |
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.
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
| category | line_items | revenue |
|---|---|---|
| Office Supplies | 62 | 47482.77 |
| Toys | 58 | 44782.9 |
| Automotive | 51 | 43055.93 |
| Food & Beverage | 60 | 42452.58 |
| Books | 61 | 40552.7 |
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.
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
| status | orders | avg_order_value |
|---|---|---|
| shipped | 21 | 2841.56 |
| refunded | 19 | 2626.22 |
| delivered | 82 | 2570.44 |
| processing | 28 | 2516.91 |
| cancelled | 27 | 2447.19 |
| pending | 23 | 2351.12 |
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.
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
| id | name | units_sold | revenue |
|---|---|---|---|
| 119 | Merino Wool Sweater | 30 | 8995.83 |
| 38 | LEGO Creator Set 742pcs | 37 | 8530.61 |
| 60 | Leather Steering Wheel Cover | 25 | 7519.96 |
| 32 | Ergonomic Desk Mat XL | 33 | 7066.04 |
| 51 | Ergonomic Desk Mat XL | 24 | 6944.33 |
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.
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
| month | orders | revenue |
|---|---|---|
| 2024-01 | 8 | 16318.57 |
| 2024-02 | 8 | 16998.61 |
| 2024-03 | 6 | 16549.31 |
| 2024-04 | 9 | 22604.85 |
| 2024-05 | 3 | 7590.89 |
| 2024-06 | 2 | 5186.49 |
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.
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
| user_id | order_count |
|---|---|
| 77 | 5 |
| 37 | 5 |
| 124 | 4 |
| 118 | 4 |
| 83 | 4 |
| 54 | 4 |
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.
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
| method | payments | collected |
|---|---|---|
| Cryptocurrency | 24 | 28333.72 |
| Debit Card | 21 | 24174.39 |
| Bank Transfer | 23 | 22173.03 |
| Apple Pay | 16 | 15593.63 |
| PayPal | 18 | 14884.42 |
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.
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
| category | revenue |
|---|---|
| Office Supplies | 47482.77 |
| Toys | 44782.9 |
| Automotive | 43055.93 |
| Food & Beverage | 42452.58 |
| Books | 40552.7 |
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.
-
1Easy How many products are there in each category? Table:
productsShow solution
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.
-
2Easy How many employees work in each city? Show the five largest offices. Table:
employeesShow solution
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.
-
3Medium What is the average salary in each department, highest first? Round to whole numbers. Table:
employeesShow solution
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).
-
4Medium For active products only, show the total units in stock per category. Table:
productsShow solution
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).
-
5Medium Count orders for every combination of year and status. Table:
ordersShow solution
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.
-
6Medium Which products have at least four reviews? Show the review count and average rating. Table:
reviewsShow solution
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.
-
7Challenging Which departments have an average salary above 125,000, and how many people work in each? Table:
employeesShow solution
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.
-
8Challenging Find the five customers who spent the most on delivered orders. Show their name, number of delivered orders and total spent. Table:
users + ordersShow solution
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
What does SQL GROUP BY do?
GROUP BY collects rows that share the same value in a column into a single group. It is almost always used with aggregate functions like COUNT or SUM to produce one summary row per group.
Which query correctly counts how many students are in each class?
You must include the grouped column (class) in the SELECT list AND in GROUP BY. COUNT(*) then counts the rows inside each group. The version without class in SELECT is valid SQL but hides which group each count belongs to.
What is the correct clause order that includes GROUP BY?
The correct SQL clause order is SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY. WHERE filters rows before grouping; HAVING filters groups after grouping. GROUP BY always comes after WHERE.
In PostgreSQL, SQL Server or MySQL, why is this query rejected? SELECT rating, COUNT(film_id) FROM films;
When a plain column (rating) is mixed with an aggregate (COUNT), every plain column must appear in GROUP BY. Without GROUP BY rating the database cannot know which rating to show. SQLite is lenient and returns an arbitrary rating instead of an error.
Which aggregate function gives the total sales amount per city?
SUM() adds up all values in a column per group. SELECT city, SUM(amount) FROM sales GROUP BY city returns the total sales for each city. COUNT counts rows, AVG averages, MAX finds the highest single value.
What does GROUP BY class, section produce?
GROUP BY accepts multiple columns separated by commas. Each unique combination of (class, section) becomes its own group — so Class 10 Section A and Class 10 Section B are two separate groups.
You want only the cities that have more than 10 orders. Which clause holds that condition?
The count exists only after the groups are built, so the condition goes in HAVING. WHERE runs before grouping and cannot use aggregate functions.
Does GROUP BY guarantee that the result is sorted by the grouped column?
GROUP BY builds groups; it makes no promise about their order. Results often look sorted, but only ORDER BY guarantees it.
What is the difference between COUNT(*) and COUNT(shipped_date) in a grouped query?
COUNT(*) counts rows. COUNT(column) counts only the rows where that column is not NULL.
You need the list of unique departments and nothing else. Which is the clearest query?
For unique values with no calculation, SELECT DISTINCT states the intent most clearly. GROUP BY department returns the same rows, but GROUP BY is the tool for when you also need a metric per group.
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.
| Pattern | SQL |
|---|---|
| Count rows per group | SELECT col, COUNT(*) FROM t GROUP BY col; |
| Sum values per group | SELECT col, SUM(amount) FROM t GROUP BY col; |
| Average per group | SELECT col, AVG(score) FROM t GROUP BY col; |
| Min and max per group | SELECT col, MIN(val), MAX(val) FROM t GROUP BY col; |
| Group by multiple columns | SELECT c1, c2, COUNT(*) FROM t GROUP BY c1, c2; |
| Filter rows, then group | SELECT 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
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.