HAVING
Hook
You can filter rows before aggregating with WHERE. You filter the aggregates
themselves with HAVING. “Players with at least 100 carries” is a HAVING
filter — the threshold is on a SUM, which doesn’t exist until after grouping.
Concept
Order of operations:
FROM/JOIN— load and combine tables.WHERE— filter individual rows.GROUP BY— bucket the survivors.HAVING— filter buckets.SELECT— pick output columns.ORDER BY— sort.
SELECT group_col, SUM(value) AS totalFROM tableWHERE row_filter -- pre-aggregationGROUP BY group_colHAVING SUM(value) > 100 -- post-aggregationORDER BY total DESC;You can use the alias total only in ORDER BY, not in HAVING (in standard
SQL). Repeat the aggregate.
Lions example
NFC North wide receivers with at least 60 receptions in the 2024 regular season — a real “starter” threshold:
SELECT player_display_name, recent_team, SUM(receptions) AS rec, SUM(receiving_yards) AS yards, SUM(receiving_tds) AS tdsFROM weekly_statsWHERE recent_team IN ('DET', 'GB', 'MIN', 'CHI') AND season = 2024 AND season_type = 'REG' AND position_group = 'WR'GROUP BY player_display_name, recent_teamHAVING SUM(receptions) >= 60ORDER BY yards DESC;You can stack multiple HAVING conditions with AND and OR:
HAVING SUM(receptions) >= 60 AND SUM(receiving_tds) >= 5Try it
Find every NFL running back in 2024 who averaged at least 4.5 yards per carry and had at least 100 carries. Group by player + team. Return name, team, carries, total yards, and YPC.
The yards-per-carry threshold is a derived metric — SUM(rushing_yards)::numeric / NULLIF(SUM(carries), 0) >= 4.5. The carries threshold is a separate HAVING
condition combined with AND.
Common mistakes
- Filtering aggregates in
WHERE.WHERE SUM(...) > 100errors out. Move toHAVING. - Filtering rows in
HAVING.HAVING recent_team = 'DET'works in Postgres but it’s slow — the database aggregates everything before throwing rows away. UseWHEREfor row-level conditions. - Using a column alias in
HAVING. Standard SQL says no; Postgres usually errors. Repeat the aggregate expression. - No
GROUP BYwithHAVING. Legal, but the query treats the whole set as one group. Usually a sign you meantGROUP BYsomething.
Quick check
- When does
WHEREexecute relative toGROUP BY? When doesHAVING? - Why can’t you use a column alias from
SELECTinsideHAVING? WHERE rushing_yards > 100vsHAVING SUM(rushing_yards) > 100— what’s the difference in meaning?