HAVING: Filtering Group Aggregates

Concept. HAVING filters groups after GROUP BY by evaluating aggregate expressions such as COUNT and AVG. WHERE filters input rows before grouping; HAVING filters the grouped result.

Intuition. "Users averaging above 4.0" cannot use WHERE, since no single row holds an average. Group the Listens by user, then HAVING AVG(rating) > 4.0 keeps the user groups that pass.

HAVING filters groups: the query GROUP BY user_id HAVING AVG(rating) > 4.0 forms one row per user (Mickey COUNT 3 AVG 4.2, Minnie 4.4, Daffy 3.9), then HAVING tests each group's average; Mickey and Minnie pass, Daffy 3.9 is below 4.0 so the whole group drops. Output: Mickey, Minnie.

Figure 1. HAVING tests each group by its aggregate after grouping; a group passes or drops whole. Of three user groups, two pass AVG(rating) > 4.0; Daffy's 3.9 average drops his whole group.

Simple HAVING

-- Find users whose average rating exceeds 4.0
SELECT                         -- Last: Select columns to output
    user_id,
    COUNT(*) AS listen_count,
    AVG(rating) AS avg_rating
FROM Listens                   -- First: Get rows
GROUP BY user_id               -- Next: Create groups
HAVING AVG(rating) > 4.0       -- Then: Filter groups by aggregate condition
ORDER BY user_id               -- Last: Sort surviving groups

Step 1: GROUP BY

user_idCOUNT(*)AVG(rating)Notes
13(4.5+4.2+3.9)/3User 1's
23(4.7+4.6+3.9)/3User 2's
33(2.9+4.9+NULL)/2User 3's

Step 2: HAVING AVG() > 4

user_idAVG(rating)AVG > 4.0
14.24.2 > 4.0
24.44.4 > 4.0
33.93.9 > 4.0

Output

user_idlisten_countavg_rating
134.2
234.4
  • GROUP BY first, HAVING second

  • FALSE = entire group gone

  • No partial elimination


HAVING with COUNT

HAVING COUNT(*) >= 2 on listens grouped by song_id: songs 1, 2 and 6 were played at least twice and stay; songs 7 and 8 were played once and drop. Output: songs 1, 2, 6.

Figure 2. Grouping listens by song_id and keeping only groups with COUNT(*) >= 2. Songs 1, 2 and 6 were played at least twice; songs 7 and 8 were played once, so they drop.

Key Points:

  • Unpopular songs eliminated

  • Threshold: 2+ plays

  • NULLs counted in COUNT(*)


HAVING with Multiple Conditions

HAVING COUNT(*) >= 3 AND AVG(rating) > 4.0: both conditions must hold. Mickey and Minnie pass both; Daffy has three listens but his 3.9 average fails the rating test, so his group drops. Output: Mickey, Minnie.

Figure 3. Two HAVING conditions joined by AND: a group survives only if it passes both. Mickey and Minnie clear COUNT(*) >= 3 and AVG(rating) > 4.0; Daffy has three listens but his 3.9 average fails the second test.

Key Points:

  • Mix any aggregates

WHERE and HAVING

WHERE filters rows, then HAVING filters groups. The query WHERE rating > 3.5 GROUP BY user_id HAVING COUNT(*) >= 2 first strikes Daffy's 2.9 and NULL on the raw rows (9 to 7), groups the survivors into Mickey 3 rows (AVG 4.2), Minnie 3 (AVG 4.4), Daffy 1 (AVG 4.9), then HAVING drops Daffy's one-row group. Output: users 1 and 2 (Mickey, Minnie).

Figure 4. WHERE and HAVING run at different times. WHERE strikes individual rows before grouping (Daffy's 2.9 and NULL go), the survivors form groups, then HAVING drops whole groups by aggregate (Daffy's single row fails COUNT(*) >= 2). WHERE picks rows, HAVING picks groups.

Step 1: WHERE rating > 3.5 · 9 rows → 7 rows (crossed-out rows fail the test; NULL is UNKNOWN, also removed)

listen_iduser_idratingrating > 3.5
114.54.5 > 3.5
214.24.2 > 3.5
313.93.9 > 3.5
424.74.7 > 3.5
524.64.6 > 3.5
623.93.9 > 3.5
732.92.9 > 3.5
834.94.9 > 3.5
93NULLNULL → unknown

Step 2: GROUP BY on Filtered Data

user_idFiltered RowsCOUNT(*)AVG(rating)
11,2,334.2
24,5,634.4
3814.9

Step 3: HAVING COUNT(*) >= 2 · 3 groups → 2 rows (HAVING crosses out groups that are too small)

user_idCOUNT(*)COUNT(*) >= 2
133 >= 2
233 >= 2
311 >= 2

Output

user_idCOUNT(*)AVG(rating)
134.2
234.4

Key Rules

  1. Execution Order: HAVING runs after GROUP BY.

  2. Aggregate Conditions: Can use aggregate functions like AVG(), COUNT()

  3. Group-Level Filter: Operates on entire groups, not individual rows

SQL Execution Pipeline

Input:  FROM
Logic:  WHERE → GROUP BY → HAVING
        (no aggregates) → (form groups) → (aggregates exist!)
Output: SELECT/ORDER BY

Common Patterns

  • Aggregate Filters: HAVING COUNT(*) > 2 keeps groups with 3+ rows

  • Average Checks: HAVING AVG(rating) > 4.0 filters by group average

  • Multiple Conditions: HAVING COUNT(*) > 1 AND AVG(rating) > 4.0

  • Column References: Can use GROUP BY columns or aggregates

Common Mistakes

HAVING on rows

-- WRONG: HAVING does not work on individual rows
SELECT * FROM Listens
HAVING rating > 4.0

-- CORRECT: Use WHERE to filter individual rows
SELECT * FROM Listens
WHERE rating > 4.0

Problem: HAVING only works with grouped data

The HAVING Alias Problem

-- WRONG: Alias not yet created when HAVING runs
SELECT user_id, AVG(rating) AS avg_r
FROM Listens
GROUP BY user_id
HAVING avg_r > 4.0

-- CORRECT: Repeat the aggregate expression in HAVING
SELECT user_id, AVG(rating) AS avg_r
FROM Listens
GROUP BY user_id
HAVING AVG(rating) > 4.0

Problem: HAVING executes before SELECT creates aliases

Wrong Filter, Wrong Place

-- WRONG: HAVING can't access individual row values
SELECT user_id, COUNT(*)
FROM Listens
GROUP BY user_id
HAVING rating > 4.0            -- Error: Which rating? Group has many!

-- CORRECT: WHERE for row conditions, HAVING for group conditions
SELECT user_id, COUNT(*)
FROM Listens
WHERE rating > 4.0             -- Filter rows BEFORE grouping
GROUP BY user_id
HAVING COUNT(*) > 2            -- Then filter groups by aggregate

Problem: HAVING should use aggregate functions or GROUP BY columns

NULL Aggregate Confusion

-- Surprising: AVG ignores NULL ratings
SELECT user_id, AVG(rating)
FROM Listens
GROUP BY user_id
HAVING AVG(rating) > 4.0       -- AVG skips NULLs (may mislead!)

-- Explicit: Show total vs rated counts for clarity
SELECT user_id, 
       COUNT(*) AS total,       -- Includes NULL rows
       COUNT(rating) AS rated,  -- Excludes NULL ratings
       AVG(rating) AS avg_rating
FROM Listens
GROUP BY user_id
HAVING COUNT(rating) > 0       -- Ensure at least 1 non-NULL rating
   AND AVG(rating) > 4.0       -- Then check average

Problem: AVG ignores NULL, which affects HAVING conditions