Tutorial

WHERE vs HAVING in SQL: Execution Order & Key Differences

Master WHERE vs HAVING in SQL. Learn why WHERE filters before GROUP BY, why aggregate functions fail in WHERE, and the full 7-step query execution order.

Anuj SainiAug 24, 20269 min read

Ask any beginner to calculate total revenue, and they will write a simple SUM(total_amount). But real businesses are not managed with a single grand total. Leaders need revenue broken down per status, per payment method, per region, and per month.

That breakdown requires GROUP BY. And the moment you need to filter those summarized groups—such as finding payment methods with over ₹150,000 in volume—you face the most tested query execution trap in technical interviews: WHERE vs HAVING.

In this guide, we break down GROUP BY mechanics, dissect the exact 7-step logical query execution order that governs all SQL databases, and demonstrate why aliases fail in WHERE clauses. Every query in this guide runs against a live PostgreSQL e-commerce dataset with 2,000 orders and 500 customers, with all metrics verified. For related database foundations, explore our SQL NULL Guide and SQL CTE Guide.



1. GROUP BY Mechanics: Collapsing Rows into Dimensional Summaries

An aggregate function (SUM, AVG, COUNT, MIN, MAX) without a GROUP BY folds the entire table into a single summary row:

sql
SELECT ROUND(SUM(total_amount), 2) AS grand_total
FROM orders;

Result: 782,905.04

To split that total across distinct dimensions, use GROUP BY. The rule of thumb for standard SQL queries: any non-aggregated column in your SELECT list must appear in your GROUP BY clause.

Revenue and Volume by Order Status

Let's group the 2,000 orders in our database by their status:

sql
SELECT status,
       COUNT(*)                   AS orders,
       ROUND(SUM(total_amount), 2) AS revenue,
       ROUND(AVG(total_amount), 2) AS avg_order
FROM orders
GROUP BY status
ORDER BY revenue DESC;

The Output

statusordersrevenueavg_order
completed1,101₹535,329.63₹486.22
cancelled284₹142,321.95₹501.13
pending223₹116,879.58₹524.12
returned192₹87,975.97₹458.21
refunded200-₹99,602.09-₹498.01

What the Numbers Reveal

  1. One summary row per distinct group: The 2,000 raw rows collapse into exactly 5 status categories.
  2. Hidden financial realities: The grand total of ₹782,905.04 hid the fact that ₹99,602.09 was paid out in refunds. Grouping exposes the underlying distribution.
  3. Refund averages are negative: In our e-commerce schema, refunds carry negative values (-₹498.01 average), which directly dragged down the overall table average.

2. Grouping by Payment Method

Swapping the dimension column lets you answer a new business question instantly. Let's analyze customer payment preferences:

sql
SELECT payment_method,
       COUNT(*)                   AS orders,
       ROUND(SUM(total_amount), 2) AS revenue
FROM orders
GROUP BY payment_method
ORDER BY revenue DESC;

The Output

payment_methodordersrevenue
credit_card446₹173,019.72
bank_transfer398₹157,213.44
crypto385₹156,611.59
paypal391₹153,070.22
debit_card380₹142,990.07

Credit card leads transaction volume at ₹173,019.72, while debit card trails at ₹142,990.07. Notice that these totals reflect net volume across all order statuses.


3. The Core Trap: Filtering Groups with HAVING vs WHERE

Suppose your stakeholder asks: "Show me only the payment methods that generated more than ₹150,000 in revenue."

Natural intuition leads most beginners to write a WHERE filter:

sql
-- ❌ THIS CODE THROWS A RUNTIME ERROR
SELECT payment_method,
       ROUND(SUM(total_amount), 2) AS revenue
FROM orders
WHERE SUM(total_amount) > 150000
GROUP BY payment_method;

The Database Error

ERROR: aggregate functions are not allowed in WHERE
LINE 4: WHERE SUM(total_amount) > 150000

The database halts immediately. Why? Because the WHERE clause evaluates individual rows before any grouping or aggregation takes place. At the time WHERE executes, SUM(total_amount) does not exist yet.

The Correct Query: Using HAVING

To filter summarized groups based on aggregate conditions, you must use HAVING:

sql
-- ✅ THE CORRECT WAY: HAVING filters groups
SELECT payment_method,
       ROUND(SUM(total_amount), 2) AS revenue
FROM orders
GROUP BY payment_method
HAVING SUM(total_amount) > 150000
ORDER BY revenue DESC;

The Filtered Result

payment_methodrevenue
credit_card₹173,019.72
bank_transfer₹157,213.44
crypto₹156,611.59
paypal₹153,070.22

Exactly 4 rows return. debit_card (₹142,990.07) was below the ₹150,000 threshold and was cleanly discarded after aggregation.

The Golden Rule of Filtering

WHERE filters individual rows before grouping occurs.
HAVING filters entire groups after aggregation completes.
Never put an aggregate function (SUM, AVG, COUNT, MIN, MAX) inside WHERE.


4. Query Execution Order: Why the Difference Exists

SQL syntax is declarative: you write clauses in one order, but the query optimizer executes them in a completely different sequence. Understanding this 7-step sequence makes database errors predictable and intuitive.

┌──────────────────────────────────────────────────────────┐
│             LOGICAL QUERY EXECUTION ORDER                │
├──────────────────────────────────────────────────────────┤
│  1. FROM & JOINS   → Retrieve and combine base tables    │
│  2. WHERE          → Filter individual rows              │
│  3. GROUP BY       → Collapse rows into distinct groups  │
│  4. HAVING         → Filter the aggregated groups        │
│  5. SELECT         → Compute columns, expressions, aliases│
│  6. DISTINCT       → Deduplicate result rows             │
│  7. ORDER BY       → Sort the final rows                 │
│  8. LIMIT / OFFSET → Truncate to the requested window    │
└──────────────────────────────────────────────────────────┘

Why Aliases Fail in WHERE and GROUP BY

Look at where SELECT sits in the pipeline (Step 5). When you write:

sql
SELECT payment_method,
       SUM(total_amount) AS total_rev -- alias created at Step 5
FROM orders                           -- Step 1
WHERE total_rev > 150000              -- ❌ Step 2: total_rev does NOT exist yet!
GROUP BY payment_method;              -- Step 3

Because WHERE runs at Step 2 and GROUP BY runs at Step 3, the alias total_rev defined in SELECT (Step 5) has not been compiled.

However, ORDER BY executes at Step 7 (after SELECT). Therefore, sorting by an alias works seamlessly:

sql
ORDER BY revenue DESC -- ✅ Step 7: revenue alias is fully available

5. Combining WHERE and HAVING in Production Queries

In professional data analysis, you almost always use WHERE and HAVING together in the same query:

  1. Use WHERE to strip out irrelevant rows early (reducing memory usage and compute cost).
  2. Use GROUP BY to summarize the remaining rows.
  3. Use HAVING to discard groups that do not meet your business threshold.

Example: High-Performing Payment Methods for Completed Orders Only

sql
SELECT payment_method,
       COUNT(*)                   AS completed_orders,
       ROUND(SUM(total_amount), 2) AS clean_revenue
FROM orders
WHERE status = 'completed'           -- Step 2: filter out refunds & cancellations first
GROUP BY payment_method              -- Step 3: group remaining rows
HAVING SUM(total_amount) > 100000    -- Step 4: keep payment methods with >₹100k completed
ORDER BY clean_revenue DESC;         -- Step 7: sort output

The Result

payment_methodcompleted_ordersclean_revenue
credit_card248₹120,412.30
bank_transfer224₹113,156.64
paypal219₹106,892.15
crypto212₹102,448.20

By filtering WHERE status = 'completed' upfront, we eliminated the negative refund balances, ensuring clean_revenue represents true realized earnings.


6. Aggregates and NULLs: Three Flavors of COUNT

A major source of aggregation confusion in technical assessments is how SQL aggregates handle NULL values. Let's observe the behavior across our 2,000-order table and 800-row reviews table.

sql
SELECT COUNT(*)                    AS all_rows,
       COUNT(shipped_date)         AS has_ship_date,
       COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;

The Three COUNTs

all_rowshas_ship_dateunique_customers
2,0001,198544
  • COUNT(*) (2,000): Counts all rows, regardless of whether any column contains NULL.
  • COUNT(column) (1,198): Counts only rows where shipped_date is NOT NULL (meaning 802 orders have not yet shipped or were cancelled).
  • COUNT(DISTINCT column) (544): Counts distinct non-NULL customer IDs. (Note: Our dataset contains 500 customers in the customer table, exposing 44 orphaned customer IDs in orders).

The Average Rating Trap

In our reviews table (800 rows), 86 reviews have a NULL rating:

sql
SELECT COUNT(*)                           AS all_reviews,
       COUNT(rating)                      AS rated_reviews,
       ROUND(AVG(rating), 3)              AS avg_real,
       ROUND(AVG(COALESCE(rating, 0)), 3) AS avg_zeroed
FROM reviews;

The Output

all_reviewsrated_reviewsavg_realavg_zeroed
8007142.9412.625

AVG(rating) skips the 86 NULL rows and calculates the average over the 714 rated reviews (2.941). If you mistakenly replace NULL with 0 using COALESCE, you invent 86 fake 0-star ratings and depress your true customer rating down to 2.625.


7. WHERE vs HAVING: Feature Comparison

Feature / Criteria

8. Summary Checklist & Practice

When writing aggregation queries, follow this mental checklist:

  1. Grain check: What does one row represent in your output? Ensure every unaggregated column in SELECT appears in GROUP BY.
  2. Filter placement: If your condition evaluates row-level fields (e.g. status = 'completed', order_date >= '2024-01-01'), place it in WHERE. If it evaluates a calculated summary (e.g. SUM(...) > 50000, COUNT(*) >= 5), place it in HAVING.
  3. Execution order awareness: Remember why aliases fail in WHERE and GROUP BY, and keep ORDER BY for final sorting.

To master SQL aggregations, test execution order traps, and build portfolio-grade skills, practice live queries in our Interactive SQL Practice Sandbox or enroll in the complete Data Analyst Career Track. For foundational tutorials, read our SQL JOIN Fan-Out Guide and SQL Date Functions Guide.

Master SQL Aggregations & Execution Order

Drill WHERE, GROUP BY, and HAVING problems in our interactive PostgreSQL sandbox, or enroll in the complete Data Analyst Career Track.

Practice Aggregations Free

Frequently Asked Questions

What is the difference between WHERE and HAVING in SQL?

WHERE filters individual rows before grouping occurs, and cannot evaluate aggregate functions like SUM() or COUNT(). HAVING filters entire groups after aggregation has completed, evaluating conditions on summarized metrics.

Why does WHERE SUM(total_amount) > 150000 throw an error?

In SQL's logical execution order, WHERE runs at step 2, while GROUP BY runs at step 3. At step 2, groups do not yet exist and aggregate functions have not been calculated. SQL engines reject aggregate functions in WHERE with 'ERROR: aggregate functions are not allowed in WHERE'.

What is the exact execution order of a SQL query?

SQL queries execute in this logical sequence: 1. FROM (and JOINs), 2. WHERE (filter rows), 3. GROUP BY (form groups), 4. HAVING (filter groups), 5. SELECT (compute expressions and aliases), 6. DISTINCT, 7. ORDER BY (sort output), 8. LIMIT / OFFSET.

Why can't I use a column alias created in SELECT inside WHERE or GROUP BY?

SELECT executes at step 5, after WHERE (step 2) and GROUP BY (step 3). Column aliases defined in SELECT do not exist yet when WHERE or GROUP BY evaluate. However, ORDER BY runs at step 7 (after SELECT), so it can reference SELECT aliases.

How do aggregates handle NULL values in GROUP BY queries?

Aggregate functions like SUM, AVG, MIN, and MAX ignore NULL values automatically. COUNT(*) counts all rows including NULLs. When grouping by a nullable column, all NULL rows are gathered into a single distinct group.

Anuj Saini

Written by

Anuj SainiFounder & Lead Instructor

Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.