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 Compiler

Result:

statusorders
Delivered16
Pending2
Shipped2
Returned2
Cancelled2

(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 Compiler

Result:

product_idunits_soldnet_revenue
101368397.15
103255982.50
105324222.15
10267794.00
10447501.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 Compiler

Result:

payment_methodstatusorders
CODDelivered2
CODReturned1
CODPending1
CardDelivered4
CardReturned1
CardCancelled1
CardShipped1
NetBankingDelivered2
UPIDelivered8
UPIShipped1
UPICancelled1
UPIPending1

(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 Compiler

Result:

monthorders
2025-07-011
2025-08-012
2025-09-012
2025-10-012
2025-11-011
2025-12-012
2026-01-012
2026-02-011

(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 Compiler

Result:

category_idproductsmin_pricemax_pricetotal_stock
131299.0031990.00302
231899.008499.00165
33549.002199.00415
42199.002995.00530
52299.00350.00210
62799.003499.00170

(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 Compiler

Result:

customer_iddelivered_orders
13
33
22
42
51

(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 Compiler

Error: 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(*) > 2 fails. Group filters go in HAVING.
  • Grouping by a column with messy values. 'Bengaluru' and 'bengaluru ' become two groups. Clean with TRIM/INITCAP first.

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 Compiler

Result:

citycustomersprime_customers
Bengaluru21
Ahmedabad10
Chennai10
Delhi11
Hyderabad11

(5 rows)

Key takeaways

  • GROUP BY makes 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.
  • WHERE filters rows before grouping; HAVING filters 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.

Newsletter

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)