Home/Learn/SQL/HAVING vs WHERE

HAVING vs WHERE

Beginner
Aggregation & Grouping

WHERE filters individual rows before grouping; HAVING filters groups after aggregation — only HAVING can use aggregate functions as filter conditions.

Overview

Understanding when SQL filters rows versus groups is fundamental. The logical execution order is: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. WHERE operates at the row level, before any grouping or aggregation takes place, so it cannot reference aggregate functions. HAVING operates at the group level, after GROUP BY has been applied, so it can filter groups based on aggregated values like AVG(salary) or COUNT(*). A common performance mistake is using HAVING to filter on non-aggregated columns — this wastes work because those rows should have been eliminated by WHERE before grouping. Push every non-aggregate condition into WHERE to reduce the rows the grouping engine must process.

WHERE Filters Rows, HAVING Filters Groups

The correct version uses WHERE to remove individual rows early, then HAVING to filter the resulting groups. The wrong version uses HAVING for both — it forces all rows through the GROUP BY before filtering.

SQL — WHERE before GROUP BY, HAVING after GROUP BY
-- Goal: find departments where active employees' average salary exceeds 70 000

-- WRONG: using HAVING to filter non-aggregated conditions (status check)
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 70000
   AND status = 'active';   -- ERROR or logical mistake: status is a row-level filter
-- In PostgreSQL this actually raises an error because status is not in GROUP BY.

-- CORRECT: WHERE eliminates inactive rows first, HAVING filters groups
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active'              -- row-level filter: runs BEFORE GROUP BY
GROUP BY department_id
HAVING AVG(salary) > 70000;         -- group-level filter: runs AFTER GROUP BY

-- The WHERE version is also faster: fewer rows enter the GROUP BY.

Combined WHERE + HAVING and Execution Order

WHERE and HAVING can coexist in the same query. Execution order: FROM → WHERE → GROUP BY → HAVING. Internalising this order prevents both correctness bugs and unnecessary performance overhead.

SQL — combined WHERE + HAVING with execution order annotated
-- Find departments where the average salary of employees hired after 2019
-- exceeds the company-wide average salary.

SELECT
    d.name         AS department,
    COUNT(e.id)    AS headcount,
    AVG(e.salary)  AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.hire_date > '2019-12-31'          -- WHERE: filter rows before grouping
GROUP BY d.name
HAVING AVG(e.salary) > (                  -- HAVING: filter groups after grouping
    SELECT AVG(salary) FROM employees WHERE hire_date > '2019-12-31'
)
ORDER BY avg_salary DESC;

-- Execution order reminder (logical, not necessarily physical):
-- 1. FROM employees JOIN departments   → combined row set
-- 2. WHERE hire_date > '2019-12-31'    → rows eliminated early
-- 3. GROUP BY d.name                   → groups formed
-- 4. HAVING AVG(salary) > (subquery)   → groups eliminated
-- 5. SELECT d.name, COUNT, AVG         → projection
-- 6. ORDER BY avg_salary DESC          → sort output

Common Mistakes

Two frequent errors: trying to use a SELECT alias in HAVING (not valid in most databases), and using HAVING without GROUP BY to filter the single implicit group.

SQL — common WHERE/HAVING mistakes and fixes
-- Mistake 1: referencing a SELECT alias in HAVING (invalid in most DBs)
-- because HAVING executes before SELECT in the logical order.
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id
HAVING avg_sal > 70000;     -- ERROR in PostgreSQL/MySQL strict mode
-- Fix: repeat the expression:
HAVING AVG(salary) > 70000;

-- Mistake 2: using WHERE to filter an aggregate (syntax error)
SELECT department_id, AVG(salary)
FROM employees
WHERE AVG(salary) > 70000    -- SYNTAX ERROR: aggregate not allowed in WHERE
GROUP BY department_id;

-- Acceptable: HAVING without GROUP BY (filters the single grand-total group)
SELECT COUNT(*), AVG(salary)
FROM employees
HAVING COUNT(*) > 100;       -- returns the row only if total headcount > 100

Key Points to Remember

  • 1WHERE executes before GROUP BY (row-level filter); HAVING executes after GROUP BY (group-level filter).
  • 2Aggregate functions (AVG, COUNT, SUM) are only valid in HAVING or SELECT — never in WHERE.
  • 3Push all non-aggregate conditions to WHERE to reduce the number of rows the GROUP BY engine processes.
  • 4Referencing a SELECT alias in HAVING is invalid in most databases because HAVING precedes SELECT in logical order.
  • 5HAVING without GROUP BY filters the single implicit group formed by aggregating all rows.
  • 6Logical execution order: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.

Interview Questions

Sign in to ask Aria
1

What is the difference between WHERE and HAVING? Can you use aggregate functions in WHERE?

EasyFlipkart
2

Write a query to find departments with more than 5 active employees and an average salary above 60 000.

EasySwiggy
3

Why is it more performant to filter on a non-aggregated column in WHERE rather than HAVING?

MediumAmazon
4

What is the logical execution order of a SQL SELECT statement and how does it explain the WHERE vs HAVING distinction?

MediumMicrosoft

Ask Aria about HAVING vs WHERE

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…