SQL GROUP BY Explained for Beginners (Examples)
Confused by SQL GROUP BY? This beginner guide explains grouping, COUNT, SUM, and HAVING with simple, copy-paste SQL examples you can run today.
Confused by SQL GROUP BY? This beginner guide explains grouping, COUNT, SUM, and HAVING with simple, copy-paste SQL examples you can run today.
If you searched for "SQL GROUP BY explained for beginners," here is the short answer: GROUP BY takes many rows that share the same value in a column and squeezes them into one summary row per value, so you can count, add up, or average them. This guide builds that idea from scratch with tiny, copy-pasteable examples, so you do not need any prior SQL knowledge beyond a basic SELECT statement.
By the end, you will be able to answer questions like "how many books do I have in each genre?" or "what is the average price per genre?" using nothing but a single SQL query.
Imagine you dump a big pile of receipts on a table and someone asks, "how much did we spend per store?" You would naturally sort the receipts into small piles, one pile per store, then add up each pile. GROUP BY does exactly that inside a database: it sorts rows into buckets based on a column you choose, and then lets you run a calculation on each bucket separately.
A calculation that works on a whole bucket of rows (instead of one row at a time) is called an aggregate function. The most common ones are COUNT, SUM, AVG, MIN, and MAX. GROUP BY and aggregate functions almost always appear together.
To follow along, create a small table called books and add a few rows. You can run this in PostgreSQL, MySQL, or any SQL tool you like — the GROUP BY syntax shown here works the same way in all of them.
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title VARCHAR(100),
genre VARCHAR(50),
format VARCHAR(20),
price NUMERIC(6,2)
);
INSERT INTO books (title, genre, format, price) VALUES
('The Hobbit', 'Fantasy', 'Paperback', 15.99),
('Dune', 'Sci-Fi', 'Hardcover', 12.50),
('1984', 'Sci-Fi', 'Paperback', 9.99),
('The Shining', 'Horror', 'Paperback', 11.00),
('It', 'Horror', 'Hardcover', 14.25),
('Foundation', 'Sci-Fi', 'Paperback', 13.75);Line by line: CREATE TABLE books defines a new table with five columns. id SERIAL PRIMARY KEY makes an auto-incrementing unique number for each row. title, genre, and format store text (VARCHAR just means "text with a maximum length"). price uses NUMERIC(6,2), which means up to 6 digits total with 2 of them after the decimal point — good for money. The INSERT INTO statement then adds six rows of sample data, one book per line, so we have something real to group.
Let's answer: "how many books do I have in each genre?"
SELECT genre, COUNT(*) AS total_books
FROM books
GROUP BY genre;SELECT genre, COUNT(*) AS total_books tells SQL to show the genre column plus a count, and to label that count column total_books (AS just renames a column in the results). FROM books says which table to read. GROUP BY genre is the key line: it collects all rows that share the same genre value into one bucket before COUNT(*) runs, so COUNT(*) counts rows per bucket instead of counting the whole table. The result has one row per genre — for example Sci-Fi with 3, Horror with 2, and Fantasy with 1 — instead of six rows for six books.
COUNT is not the only aggregate function. You can also total up or average a numeric column per group.
SELECT genre,
SUM(price) AS total_price,
AVG(price) AS avg_price
FROM books
GROUP BY genre;SUM(price) adds up the price column within each genre bucket, and AVG(price) divides that total by how many rows are in the bucket to get an average. Because both SUM and AVG appear in the same SELECT as genre, and genre is also listed in GROUP BY, this query is valid — every column in SELECT is either grouped or wrapped in an aggregate function.
| Function | What it calculates | Example |
|---|---|---|
| COUNT(*) | Number of rows in each group | COUNT(*) AS total_books |
| SUM(column) | Total of a numeric column in each group | SUM(price) AS total_price |
| AVG(column) | Average of a numeric column in each group | AVG(price) AS avg_price |
| MIN(column) | Smallest value in each group | MIN(price) AS cheapest |
| MAX(column) | Largest value in each group | MAX(price) AS most_expensive |
WHERE filters individual rows before grouping happens, so it cannot check an aggregate result like COUNT(*) — the count does not exist yet at that point in the query. To filter on an aggregate value, SQL gives you a separate keyword: HAVING, which runs after the groups are built.
SELECT genre, COUNT(*) AS total_books
FROM books
GROUP BY genre
HAVING COUNT(*) > 1;This query groups books by genre, counts each group, and then HAVING COUNT(*) > 1 throws away any genre that has only one book. In our sample data, that removes Fantasy (only "The Hobbit") and keeps Sci-Fi and Horror. Think of it this way: WHERE removes rows before grouping, HAVING removes whole groups after grouping.
You are not limited to one column. Grouping by two columns creates one bucket per unique combination of both values.
SELECT genre, format, COUNT(*) AS total_books
FROM books
GROUP BY genre, format;GROUP BY genre, format now creates a separate bucket for every genre-and-format pair — Sci-Fi Paperback is a different bucket from Sci-Fi Hardcover. This is useful when a single column is too broad and you need a finer breakdown, for example "books per genre, split by paperback versus hardcover."
If you are not yet comfortable with basic filtering, it helps to revisit SELECT and WHERE first, since GROUP BY builds directly on top of those two clauses.
Not sure how SELECT and WHERE work yet? Start with our beginner guide before tackling GROUP BY.
Read: SQL SELECT and WHERE Explained for BeginnersWHERE filters individual rows before grouping happens, and it cannot reference aggregate functions like COUNT() or SUM(). HAVING filters entire groups after GROUP BY has combined the rows, so it can check aggregate results. Use WHERE to remove rows early, and HAVING to remove whole groups afterward.
Yes, though it is unusual. On its own, GROUP BY just removes duplicate combinations of the grouped columns, similar to SELECT DISTINCT. Its real power shows up when you pair it with COUNT, SUM, AVG, MIN, or MAX.
Databases like PostgreSQL require every column in SELECT to be either listed in GROUP BY or wrapped in an aggregate function. Each result row now represents a whole group of original rows, so SQL needs a clear rule for reducing any non-grouped column down to one value.
For the grouping result itself, no — GROUP BY genre, format produces the same groups as GROUP BY format, genre. Column order mainly affects readability, though in some databases it can influence how indexes are used to speed up the query.
Yes. Add an ORDER BY clause after GROUP BY, and after HAVING if you use one, to sort the grouped results. For example, ORDER BY total_books DESC would list the biggest groups first.
GROUP BY feels tricky the first time, but it comes down to one idea: put similar rows in the same bucket, then run a calculation on each bucket. Practice with your own small table, try COUNT and SUM first, and add HAVING once grouping itself feels natural.
Want help turning raw data into clear reports for your business?
Talk to FepiqOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
What is an index in SQL? A plain-English guide with copy-paste examples showing how indexes speed up queries and when you actually need one.
Learn JavaScript DOM manipulation for beginners: select elements, change text and styles, and handle clicks with simple, copy-paste code examples.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.