All posts
PostgreSQLOctober 8, 20267 min read

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.

F
Fepiq Team
Fepiq

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.

What does GROUP BY do in PostgreSQL?

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.

FunctionWhat it returnsExample question
COUNT()Number of rowsHow many orders per customer?
SUM()Total of a number columnHow much money per product?
AVG()Average of a number columnWhat is the average price per category?
MIN()Smallest valueWhat is the cheapest item per category?
MAX()Largest valueWhat is the newest order per customer?

Set up a small practice table

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.

sql
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.

Step 1: GROUP BY with COUNT

Let us answer the question: how many orders did each customer make?

sql
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).

Step 2: GROUP BY with SUM

Now let us find how much money each customer spent in total.

sql
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.

Step 3: GROUP BY with AVG, MIN and MAX

You can use several aggregate functions in one query. This gives the full picture for each product.

sql
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).

The most common GROUP BY mistake

Beginners often write a query like this and get an error.

sql
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.

sql
-- 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.

Step 4: Filter rows with WHERE before grouping

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.

sql
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.

Step 5: Filter groups with HAVING

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.

sql
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.

Step 6: Sort the grouped results

Finally, add ORDER BY to show the biggest spenders first.

sql
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.

Query order cheat sheet

SQL clauses must be written in a fixed order. This is the order for a full grouped query.

  1. SELECT: which columns and aggregates to show
  2. FROM: which table to read
  3. WHERE: filter single rows before grouping
  4. GROUP BY: make the groups
  5. HAVING: filter the groups
  6. ORDER BY: sort the final result

Common beginner tips

  • COUNT(*) counts all rows in a group. COUNT(column) skips rows where that column is empty (NULL).
  • Use short, clear aliases with AS so your results are easy to read.
  • Start with a simple GROUP BY on one column, check the result, then add more.
  • If you get a 'must appear in the GROUP BY clause' error, check every column in SELECT.

Frequently asked questions

What does GROUP BY do in PostgreSQL?+

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.

What is the difference between WHERE and HAVING in PostgreSQL?+

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.

Can I GROUP BY more than one column?+

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.

Why do I get 'must appear in the GROUP BY clause or be used in an aggregate function'?+

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().

How do I count rows per group in PostgreSQL?+

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 PostgreSQL

Want the same idea in plain SQL? See our GROUP BY guide that works on any database.

Read: SQL GROUP BY Explained

Need help building a database-powered website or app? The Fepiq team is happy to talk.

Contact Fepiq

Get new posts in your inbox

Occasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.

Keep reading

Related posts

All posts

Let's build something

Ready to ship your next product with Fepiq?

Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.