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
SUMorCOUNT).- 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 theWHEREstep. 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

