PostgreSQL GROUP BY Explained for Beginners (Examples)
PostgreSQL GROUP BY explained for beginners: learn to count, sum and average rows by group with simple examples, plus how to fix the common GROUP BY error.
PostgreSQL GROUP BY explained for beginners: learn to count, sum and average rows by group with simple examples, plus how to fix the common GROUP BY error.
PostgreSQL GROUP BY lets you split rows into groups and then count, add up, or average each group. If you want to know how many orders each customer made, or how much each product earned, GROUP BY is the tool. In this beginner guide you will learn what GROUP BY does in PostgreSQL, how to use it with COUNT, SUM and AVG, and the one mistake almost everyone makes at first.
You do not need any experience. Every term is explained the first time it appears, and every example can be copied into psql (the PostgreSQL command-line tool) or any other PostgreSQL client.
A normal SELECT query returns one result row for every row in the table. GROUP BY changes that. It puts rows that share the same value into one bucket, called a group, and returns one result row per group.
Think about a box of coloured pencils. If you sort them into piles by colour, you get one pile per colour. GROUP BY sorts table rows into piles by a column of your choice. Then you can ask a question about each pile, such as how many pencils are in it.
The questions are answered by aggregate functions. An aggregate function takes many values and returns one single value. The most common ones are in this table.
| Function | What it returns | Example question |
|---|---|---|
| COUNT() | Number of rows | How many orders per customer? |
| SUM() | Total of a number column | How much money per product? |
| AVG() | Average of a number column | What is the average price per category? |
| MIN() | Smallest value | What is the cheapest item per category? |
| MAX() | Largest value | What is the newest order per customer? |
Let us create a table called orders and add a few rows. If you have not made a table before, our guide on creating tables is linked further down.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer TEXT NOT NULL,
product TEXT NOT NULL,
amount NUMERIC(8,2) NOT NULL
);
INSERT INTO orders (customer, product, amount) VALUES
('Anna', 'Keyboard', 40.00),
('Anna', 'Mouse', 15.50),
('Ben', 'Keyboard', 40.00),
('Ben', 'Monitor', 180.00),
('Ben', 'Mouse', 15.50),
('Chloe', 'Monitor', 180.00);Line by line: CREATE TABLE makes a new table named orders. The id column is a number that PostgreSQL fills in for you (SERIAL) and it identifies each row (PRIMARY KEY). The customer and product columns hold text. The amount column holds a number with up to 8 digits and 2 after the decimal point (NUMERIC(8,2)). NOT NULL means the value can never be empty. INSERT INTO adds six rows, one per order.
Let us answer the question: how many orders did each customer make?
SELECT customer, COUNT(*) AS total_orders
FROM orders
GROUP BY customer;SELECT customer says we want to see the customer name. COUNT(*) counts the rows in each group. AS total_orders gives that count a friendly column name. FROM orders names the table. GROUP BY customer makes one group per customer. The result is Anna 2, Ben 3 and Chloe 1 (the row order may vary).
Now let us find how much money each customer spent in total.
SELECT customer, SUM(amount) AS total_spent
FROM orders
GROUP BY customer;The structure is the same. Only the function changed. SUM(amount) adds up the amount column inside each group. Anna gets 55.50, Ben gets 235.50 and Chloe gets 180.00.
You can use several aggregate functions in one query. This gives the full picture for each product.
SELECT product,
COUNT(*) AS times_sold,
AVG(amount) AS average_amount,
MAX(amount) AS biggest_sale
FROM orders
GROUP BY product;Here we group by product instead of customer. For each product we count the sales, find the average amount, and find the biggest amount. Because a keyboard always costs 40.00 in our data, its average and biggest sale are both 40.00.
Tip: AVG often returns many decimal places. Wrap it in ROUND to tidy it up, for example ROUND(AVG(amount), 2).
Beginners often write a query like this and get an error.
SELECT customer, product, COUNT(*)
FROM orders
GROUP BY customer;PostgreSQL replies with: column "orders.product" must appear in the GROUP BY clause or be used in an aggregate function. The reason is simple. Anna has two products, but the result has only one row for Anna. PostgreSQL cannot decide which product to show.
The rule to remember: every column in SELECT must either be listed in GROUP BY or be inside an aggregate function like COUNT or SUM. You can fix the query in two ways.
-- Fix 1: group by both columns
SELECT customer, product, COUNT(*)
FROM orders
GROUP BY customer, product;
-- Fix 2: remove product from SELECT
SELECT customer, COUNT(*)
FROM orders
GROUP BY customer;Fix 1 makes one group for each customer and product pair. Fix 2 removes the column that caused the problem. Pick the one that matches the question you are asking.
Use WHERE when you want to ignore some rows before they are put in groups. For example, let us only look at orders above 20.
SELECT customer, SUM(amount) AS total_spent
FROM orders
WHERE amount > 20
GROUP BY customer;PostgreSQL first throws away every row where amount is 20 or less. Then it groups what is left and adds it up. The order is always: WHERE first, then GROUP BY.
What if you want to filter the groups themselves, such as customers who spent more than 100? WHERE cannot do this, because it runs before the groups exist. Use HAVING instead.
SELECT customer, SUM(amount) AS total_spent
FROM orders
GROUP BY customer
HAVING SUM(amount) > 100;PostgreSQL builds the groups, adds up each one, and then keeps only the groups where the total is above 100. Ben (235.50) and Chloe (180.00) stay. Anna (55.50) is removed.
Finally, add ORDER BY to show the biggest spenders first.
SELECT customer, SUM(amount) AS total_spent
FROM orders
GROUP BY customer
ORDER BY total_spent DESC;ORDER BY total_spent DESC sorts the result from the highest total to the lowest. DESC means descending. Leave it out, or use ASC, for the smallest first.
SQL clauses must be written in a fixed order. This is the order for a full grouped query.
GROUP BY puts rows with the same value in a column into one group and returns one result row per group. You normally combine it with an aggregate function such as COUNT, SUM or AVG to calculate something for each group.
WHERE filters individual rows before they are grouped. HAVING filters whole groups after the aggregate values are calculated. Use HAVING when your condition uses COUNT, SUM, AVG, MIN or MAX.
Yes. Write the columns separated by commas, such as GROUP BY customer, product. PostgreSQL then makes one group for each unique combination of those values.
Your SELECT list has a column that is neither in GROUP BY nor inside an aggregate function. PostgreSQL cannot choose one value to show for the whole group. Add the column to GROUP BY or wrap it in a function like MAX().
Use COUNT(*) with GROUP BY. For example, SELECT customer, COUNT(*) FROM orders GROUP BY customer returns the number of orders for every customer.
New to PostgreSQL SELECT queries? Read our step-by-step guide on selecting data before you practise grouping.
Read: How to Select Data in PostgreSQLWant the same idea in plain SQL? See our GROUP BY guide that works on any database.
Read: SQL GROUP BY ExplainedNeed help building a database-powered website or app? The Fepiq team is happy to talk.
Contact FepiqOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
Learn SQL CASE WHEN step by step. This beginner guide shows how to label, group and sort data with simple examples you can copy and run today.
PostgreSQL ILIKE explained for beginners: search text without worrying about case, learn % and _ wildcards, and copy simple examples you can run today.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.