All 40 lessons
Lesson 14 of 40 · Aggregation
SQL GROUP BY
By Skillancy Editorial Team
Every example runs on the ShopKart dataset, already loaded in the free SQL Compiler. Click Try it in the SQL Compiler under a query to open it there, then press Run.
GROUP BY splits rows into groups that share a value and runs an aggregate on each group. Instead of one total for the whole table, you get one total per status, per product, per city or per month.
Syntax
SELECT group_column, AGGREGATE(other_column)
FROM table_name
WHERE row_condition
GROUP BY group_column
ORDER BY ...;Examples
1. Count per group
How many orders are in each status?
SELECT status,
COUNT(*) AS orders
FROM orders
GROUP BY status
ORDER BY orders DESC;Try it in the SQL CompilerResult:
| status | orders |
|---|---|
| Delivered | 16 |
| Pending | 2 |
| Shipped | 2 |
| Returned | 2 |
| Cancelled | 2 |
(5 rows)
Think of it as sorting the 24 orders into five piles by status, then counting each pile.
2. Sum per group
Units sold and net revenue per product:
SELECT product_id,
SUM(quantity) AS units_sold,
ROUND(SUM(quantity * unit_price * (1 - discount_pct / 100)), 2) AS net_revenue
FROM order_items
GROUP BY product_id
ORDER BY net_revenue DESC
LIMIT 5;Try it in the SQL CompilerResult:
| product_id | units_sold | net_revenue |
|---|---|---|
| 101 | 3 | 68397.15 |
| 103 | 2 | 55982.50 |
| 105 | 3 | 24222.15 |
| 102 | 6 | 7794.00 |
| 104 | 4 | 7501.05 |
(5 rows)
You'll replace product_id with the product name once you learn joins.
3. Group by multiple columns
Orders by payment method and status:
SELECT payment_method,
status,
COUNT(*) AS orders
FROM orders
GROUP BY payment_method, status
ORDER BY payment_method, orders DESC;Try it in the SQL CompilerResult:
| payment_method | status | orders |
|---|---|---|
| COD | Delivered | 2 |
| COD | Returned | 1 |
| COD | Pending | 1 |
| Card | Delivered | 4 |
| Card | Returned | 1 |
| Card | Cancelled | 1 |
| Card | Shipped | 1 |
| NetBanking | Delivered | 2 |
| UPI | Delivered | 8 |
| UPI | Shipped | 1 |
| UPI | Cancelled | 1 |
| UPI | Pending | 1 |
(12 rows)
Each unique combination becomes its own group.
4. Group by an expression
Monthly order count, using DATE_TRUNC from the date functions lesson:
SELECT DATE_TRUNC('month', order_date)::date AS month,
COUNT(*) AS orders
FROM orders
WHERE order_date >= '2025-07-01'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;Try it in the SQL CompilerResult:
| month | orders |
|---|---|
| 2025-07-01 | 1 |
| 2025-08-01 | 2 |
| 2025-09-01 | 2 |
| 2025-10-01 | 2 |
| 2025-11-01 | 1 |
| 2025-12-01 | 2 |
| 2026-01-01 | 2 |
| 2026-02-01 | 1 |
(8 rows)
5. Several aggregates per group
A quick profile of each category's catalogue:
SELECT category_id,
COUNT(*) AS products,
MIN(price) AS min_price,
MAX(price) AS max_price,
SUM(stock_qty) AS total_stock
FROM products
GROUP BY category_id
ORDER BY category_id;Try it in the SQL CompilerResult:
| category_id | products | min_price | max_price | total_stock |
|---|---|---|---|---|
| 1 | 3 | 1299.00 | 31990.00 | 302 |
| 2 | 3 | 1899.00 | 8499.00 | 165 |
| 3 | 3 | 549.00 | 2199.00 | 415 |
| 4 | 2 | 199.00 | 2995.00 | 530 |
| 5 | 2 | 299.00 | 350.00 | 210 |
| 6 | 2 | 799.00 | 3499.00 | 170 |
(6 rows)
6. WHERE before GROUP BY
Only delivered orders, grouped by customer:
SELECT customer_id,
COUNT(*) AS delivered_orders
FROM orders
WHERE status = 'Delivered'
GROUP BY customer_id
ORDER BY delivered_orders DESC, customer_id
LIMIT 5;Try it in the SQL CompilerResult:
| customer_id | delivered_orders |
|---|---|
| 1 | 3 |
| 3 | 3 |
| 2 | 2 |
| 4 | 2 |
| 5 | 1 |
(5 rows)
WHERE removes rows first; then the remaining rows are grouped.
Common mistakes
Selecting a column that isn't grouped or aggregated:
SELECT status, payment_method, COUNT(*)
FROM orders
GROUP BY status;Try it in the SQL CompilerError: column "orders.payment_method" must appear in the GROUP BY clause or be used in an aggregate function
Each status group contains several payment methods, so SQL can't pick one. Add payment_method to GROUP BY or aggregate it.
- Filtering groups with WHERE.
WHERE COUNT(*) > 2fails. Group filters go in HAVING. - Grouping by a column with messy values.
'Bengaluru'and'bengaluru 'become two groups. Clean withTRIM/INITCAPfirst.
Practice question
For each city, show how many customers ShopKart has and how many of them are Prime members. Sort by customer count, highest first.
Show solution
SELECT city,
COUNT(*) AS customers,
COUNT(*) FILTER (WHERE is_prime) AS prime_customers
FROM customers
GROUP BY city
ORDER BY customers DESC, city
LIMIT 5;Try it in the SQL CompilerResult:
| city | customers | prime_customers |
|---|---|---|
| Bengaluru | 2 | 1 |
| Ahmedabad | 1 | 0 |
| Chennai | 1 | 0 |
| Delhi | 1 | 1 |
| Hyderabad | 1 | 1 |
(5 rows)
Key takeaways
GROUP BYmakes aggregates work per group instead of per table.- Every selected column must be grouped or aggregated.
- You can group by several columns or by an expression like
DATE_TRUNC. WHEREfilters rows before grouping;HAVINGfilters groups after.
Frequently asked questions
What does GROUP BY do in SQL?
GROUP BY puts rows with the same value in the listed columns into one group, so aggregate functions like COUNT or SUM are calculated per group instead of over the whole table.
Must every SELECT column be in GROUP BY?
Every column in SELECT must either be listed in GROUP BY or be inside an aggregate function. PostgreSQL allows an exception when you group by a table's primary key.
Does GROUP BY sort the results?
Not reliably. Add ORDER BY if you need a particular order.
Keep learning SQL
One email a week with SQL and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.
Prefer a one-page summary? Download the free SQL cheat sheet (PDF)