Fixing PostgreSQL ERROR: aggregate functions are not allowed in WHERE

beginner🐘 PostgreSQL2026-05-28| PostgreSQL (All versions), Linux/macOS/Windows, SQL Clients like psql, pgAdmin, or DBeaver.

Error Message

ERROR: aggregate functions are not allowed in WHERE
#postgresql#sql#aggregate#where#having#count#sum

The ProblemYou're trying to filter your query results based on a calculation—like SUM(), COUNT(), or AVG()—but PostgreSQL isn't having it. You likely wrote a query that looks like this:

-- This query will throw an error
SELECT user_id, COUNT(order_id)
FROM orders
WHERE COUNT(order_id) > 10
GROUP BY user_id;

The moment you hit execute, you're met with this roadblock:

ERROR: aggregate functions are not allowed in WHERE

Why This HappensThink of a SQL query as a factory assembly line. PostgreSQL doesn't process your command from top to bottom. Instead, it follows a strict logical sequence to ensure data integrity. Here is the order of operations:

  • FROM / JOIN: The database gathers the raw materials (your tables).- WHERE: It discards individual rows that don't match your criteria before any grouping happens.- GROUP BY: It organizes the remaining rows into piles or buckets.- Aggregation: It performs math on those buckets (calculating SUM or COUNT).- HAVING: It filters the buckets based on the math results.- SELECT: Finally, it decides which columns to show you.The error occurs because you're trying to use a group-level calculation (the aggregate) in the WHERE step. At that point, the database hasn't even created the groups yet. It can't filter based on a number that doesn't exist yet.

Solution 1: The HAVING ClauseThe HAVING clause is your go-to tool for this scenario. It acts exactly like a WHERE clause, but it waits until after the grouping is finished to do its job.

The fix:- Strip the aggregate function out of your WHERE clause.- Insert a HAVING clause directly after your GROUP BY statement.```

-- The Correct Way SELECT user_id, COUNT(order_id) as total_orders FROM orders GROUP BY user_id HAVING COUNT(order_id) > 10;


## Solution 2: Use a Common Table Expression (CTE)For complex reports—like finding customers who spent more than $5,000 across 20 different transactions—a CTE (the `WITH` clause) keeps your code readable. It essentially creates a temporary table you can query normally.

-- Using a CTE for clarity WITH customer_stats AS ( SELECT user_id, SUM(amount) as total_spent FROM payments GROUP BY user_id ) SELECT * FROM customer_stats WHERE total_spent > 5000;


In this version, the aggregation is tucked away inside the `customer_stats` block. By the time the final `SELECT` runs, `total_spent` is treated like a standard data column, allowing you to use a familiar `WHERE` filter.
## How to Verify the FixDouble-checking your logic is easy. If your query claims a user has 12 orders, run a quick sanity check for that specific ID:

SELECT COUNT(*) FROM orders WHERE user_id = 742; -- Replace with an ID from your results


If the manual count matches your aggregated result, your query logic is solid.
## Pro-Tips for Performance- **WHERE is for rows:** Use it to drop irrelevant data early (e.g., `WHERE status = 'active'`). This makes your query faster.- **HAVING is for groups:** Use it only for calculations that require looking at multiple rows at once.- **Combine them:** Don't put everything in `HAVING`. Filtering out 1,000,000 'cancelled' rows with `WHERE` is much faster than grouping them all and filtering them later.Example of a high-performance query:

SELECT category, AVG(price) FROM products WHERE in_stock = true -- Row-level filter (Fast) GROUP BY category HAVING AVG(price) > 150; -- Group-level filter

Related Error Notes