PostgreSQL HAVING
Filter grouped results based on aggregate function conditions
🔍 What is HAVING?
HAVING filters groups created by GROUP BY based on aggregate function results. Unlike WHERE which filters individual rows, HAVING filters entire groups after aggregation is performed.
-- Basic HAVING syntax
SELECT column, aggregate_function(column)
FROM table
GROUP BY column
HAVING aggregate_function(column) condition;
Key HAVING Concepts
Filter Groups
Filter results after grouping
SELECT city, COUNT(*)
FROM customers
GROUP BY city
HAVING COUNT(*) > 5;
Aggregate Conditions
Use aggregate functions in conditions
SELECT category, AVG(price)
FROM products
GROUP BY category
HAVING AVG(price) > 100;
HAVING vs WHERE
WHERE filters rows, HAVING filters groups
-- WHERE: before grouping
-- HAVING: after grouping
SELECT city, COUNT(*)
FROM customers
WHERE active = true
GROUP BY city
HAVING COUNT(*) > 3;
Multiple Conditions
Combine multiple HAVING conditions
SELECT department, AVG(salary)
FROM employees
GROUP BY department
HAVING AVG(salary) > 50000
AND COUNT(*) > 5;
🔹 Basic HAVING Example
Find cities with more than 2 customers. HAVING filters the grouped results, showing only cities that meet the count condition after GROUP BY has organized the data.
-- Sample table
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(50)
);
-- Insert sample data
INSERT INTO customers (name, city) VALUES
('John', 'Boston'),
('Jane', 'Boston'),
('Mike', 'Seattle'),
('Sarah', 'Boston'),
('Tom', 'Seattle');
-- HAVING query
SELECT
city,
COUNT(*) AS customer_count
FROM customers
GROUP BY city
HAVING COUNT(*) > 2;
Output:
| city | customer_count |
|---|---|
| Boston | 3 |
🔹 HAVING with SUM and AVG
Filter customers based on their total spending or average order value. HAVING lets you identify high-value customers by applying conditions to aggregated sales data.
-- Find customers with total purchases over $500
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spent,
AVG(amount) AS avg_order
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 500
ORDER BY total_spent DESC;
Output:
| customer_id | order_count | total_spent | avg_order |
|---|---|---|---|
| 1 | 4 | 750.00 | 187.50 |
| 3 | 3 | 600.00 | 200.00 |
🔹 WHERE vs HAVING
WHERE filters individual rows before grouping, while HAVING filters groups after aggregation. You can use both together: WHERE narrows down rows first, then HAVING filters the grouped results.
-- Using both WHERE and HAVING
SELECT
category,
COUNT(*) AS product_count,
AVG(price) AS avg_price
FROM products
WHERE in_stock = true -- Filter rows BEFORE grouping
GROUP BY category
HAVING AVG(price) > 50 -- Filter groups AFTER grouping
ORDER BY avg_price DESC;
Output:
| category | product_count | avg_price |
|---|---|---|
| Electronics | 8 | 299.99 |
| Furniture | 5 | 149.99 |
🔹 Multiple HAVING Conditions
Combine multiple conditions in HAVING clause using AND or OR operators. This allows complex filtering of grouped data based on multiple aggregate criteria simultaneously.
-- Find departments with specific criteria
SELECT
department,
COUNT(*) AS employee_count,
AVG(salary) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY department
HAVING COUNT(*) >= 5
AND AVG(salary) > 60000
AND MAX(salary) < 150000
ORDER BY avg_salary DESC;
Output:
| department | employee_count | avg_salary | max_salary |
|---|---|---|---|
| Engineering | 8 | 85000 | 120000 |
| Sales | 6 | 70000 | 95000 |
🔹 HAVING with MIN and MAX
Use HAVING with MIN and MAX functions to filter groups based on minimum or maximum values. This helps identify groups with specific range characteristics in your data.
-- Find product categories with price range criteria
SELECT
category,
COUNT(*) AS product_count,
MIN(price) AS lowest_price,
MAX(price) AS highest_price
FROM products
GROUP BY category
HAVING MIN(price) > 20
AND MAX(price) < 1000;
Output:
| category | product_count | lowest_price | highest_price |
|---|---|---|---|
| Accessories | 12 | 25.00 | 299.99 |
| Clothing | 20 | 29.99 | 199.99 |