SQL Lesson 5: GROUP BY and HAVING

📎 This article includes 2 downloadable practice files ↓

⏱ 2 min read

📘 SQL Beginner Course · Lesson 5 of 12

In this article
  1. One row per group
  2. The rule
  3. More examples
  4. Group by two columns
  5. HAVING: filter the groups
  6. Clause order
  7. Where beginners go wrong
  8. Practice

Lesson 4 gave one total for the whole table. Reports need totals per something: per region, per product, per month. That’s GROUP BY, the SQL version of a pivot table.

One row per group

SELECT region, COUNT(*) AS customers
FROM customers
GROUP BY region
ORDER BY region;

The database sorts rows into groups by region and calculates COUNT for each.

The rule

Every column in SELECT must either be in GROUP BY or inside an aggregate (COUNT, SUM…). SELECT region, name, COUNT(*) … GROUP BY region doesn’t make sense: which name would it show for North?

More examples

SELECT status, COUNT(*) AS orders FROM orders GROUP BY status ORDER BY orders DESC;

SELECT category, COUNT(*) AS products, ROUND(AVG(price), 0) AS avg_price
FROM products GROUP BY category;

SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders
FROM orders GROUP BY month ORDER BY month;

strftime('%Y-%m', …) turns a date into “2026-07” in SQLite. (SQL Server: FORMAT(order_date,'yyyy-MM'); PostgreSQL: to_char(order_date,'YYYY-MM').)

Group by two columns

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

HAVING: filter the groups

SELECT customer_id, COUNT(*) AS orders
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 28;
WHERE HAVING
Filters Rows, before grouping Groups, after grouping
Can use aggregates? No Yes
Example WHERE status = ‘Delivered’ HAVING SUM(qty) > 100
💡 Put every condition you can into WHERE (cheaper, fewer rows to group) and only aggregate conditions into HAVING.

Clause order

SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT ...;

Where beginners go wrong

Mistake Fix
Non-grouped column in SELECT Add it to GROUP BY or aggregate it
WHERE COUNT(*) > 5 HAVING COUNT(*) > 5
Months in the wrong order Group by ‘YYYY-MM’, not month names

Practice

Download shop.db and this lesson’s exercise file below. Open the database in the free DB Browser for SQLite (or sqlite3 shop.db), paste the exercises into the Execute SQL tab and write a query for each task. The expected result is printed under every task; answers are at the bottom of the file.

📎 Practice files for this article

  • 📄
    Practice database (SQLite)shop.db: 12 customers, 10 products and 300 orders (April-September 2026).
    ⬇ DB · 28 KB
  • 🗃️
    Lesson 5 exercisesTasks with the expected result under each, and answers at the bottom.
    ⬇ SQL · 2 KB

Free to use for learning. Files with macros (.bas) are plain text — import them with Alt+F11 → File → Import File, and always test on a copy.

✨ Ask AI about this article

Stuck on a step? Ask a question and the AI answers using this article.

Free · AI can be wrong