Module 2 › Session 14 · GROUP BY & HAVING

GROUP BY & HAVING: Categorical Grouping & Filtering

Partition 1,50,000 orders into category buckets, master the Golden Rule of SQL grouping, and filter groups with HAVING.

Jo class mein bola, yahan nukte mein likha hai.
What I said in class, written here as key points.

← All NukteSELECT * FROM TheShiraverse
MODULE 2 · SESSION 14

GROUP BY & HAVING:
Grouping & Filtering

Partition 1,50,000 orders into category buckets, master the Golden Rule of SQL grouping, construct monthly cohort trends, and filter groups with HAVING.

23
Scenes
1
Practice Arena
4
Practice Drills
5
Interview Questions

Today’s Agenda — GROUP BY & HAVING

From whole-table metric summaries to multi-dimensional categorical reporting

Pillar 01
📊 Grouping Fundamentals
01 Categorical partitioning piles
02 The Golden Rule of SQL grouping
03 Multi-column category combinations
Pillar 02
📅 Dynamic Bucketing
04 Custom CASE expression tiers
05 DATE_TRUNC monthly cohorts
06 Presentation formatting alignment
Pillar 03
⚖ HAVING Group Filtering
07 WHERE (Bouncer) vs HAVING (Judge)
08 Post-aggregation threshold limits
09 Query performance synergy
Pillar 04
⚙ Execution Lifecycle
10 The 8-stage physical pipeline
11 Alias visibility lifecycle
12 Grouping compiler traps & fixes
💡
Why this matters: RetailMart has 1,50,000 orders across 200 physical stores. Whole-table totals show macro health, but operational leadership manages store performance, regional trends, and fulfillment bottlenecks individually. GROUP BY segments data into category buckets and HAVING filters aggregated summaries.

Where We Left Off: Table-Wide Aggregates to Categorical Piles

Bridging whole-table executive funnels and multi-dimensional category analysis

🔢 SESSION 13 • COMPLETED

Whole-Table Metrics

-- Session 13: Whole-table executive metrics
SELECT
COUNT(*) AS total_orders,
ROUND(SUM(net_total), 2) AS revenue,
ROUND(AVG(net_total), 2) AS avg_ticket
FROM sales.orders;
-- ₹110.87 crore revenue across all stores
Session 13 collapsed whole tables into single executive metric rows. But macro totals cannot tell us which stores are thriving and which need operational intervention.
✓ Whole-table metrics locked; categorical grouping awaits
📊 SESSION 14 • THE MISSION

Categorical Buckets & Filtering

-- “Store scorecards for stores over ₹6 crore”
SELECT store_id,
COUNT(*) AS orders,
ROUND(SUM(net_total), 2) AS revenue
FROM sales.orders
GROUP BY store_id
HAVING SUM(net_total) > 60000000
ORDER BY revenue DESC;
GROUP BY partitions 1,50,000 orders into discrete category piles. HAVING filters aggregated summaries post-calculation to discard low-volume noise.
📊 Today: GROUP BY, The Golden Rule, DATE_TRUNC, HAVING
💡
The Core Rule: WHERE filters individual records before grouping occurs (the Bouncer). HAVING filters aggregated group metrics after calculation (the Judge). You cannot evaluate SUM() or COUNT() inside WHERE.
🏪
The 200-Store Scorecard Crisis MANUAL ITERATION • CONSOLE FLOOD
RETAIL OPERATIONS • METRIC AGGREGATION EXAMPLE STORY • NAMES & NUMBERS MADE UP ⚠ 200 STORES REPORT DUE IN 10 MIN
STEP 01 • THE INCIDENT
vp-retail-ops 04:15 PM
“Executive board needs the flagship store scorecard for the 5:00 PM review! Total orders and revenue for all 200 stores, filtering only stores above ₹6 crore. Analyst is running 200 separate WHERE queries and the console is crashing!”
Analyst manually ran WHERE store_id = 1, then WHERE store_id = 2... taking 3 hours and cluttering reports.
STEP 02 • ROOT CAUSES
⚠ 200 Repetitive Queries
Manual Iteration Trap
Writing individual queries with WHERE store_id = N requires 200 database round-trips and hours of manual copy-pasting.
⚡ Pre-Aggregate Filter
WHERE SUM(...) Fails
Trying to write WHERE SUM(net_total) > 60000000 throws a syntax error because WHERE runs before grouping begins.
⏱ Missed Board Review
3 Hours of Manual Work
Executive review started without regional scorecards because manual spreadsheet consolidation lagged behind.
STEP 03 • THE SQL PARTITION FIX
GROUP BY + HAVING
🐙 Categorical Partitioning
SELECT
  store_id,
  COUNT(*) AS total_orders,
  ROUND(SUM(net_total), 2) AS total_revenue
FROM sales.orders
GROUP BY store_id
HAVING SUM(net_total) > 60000000
ORDER BY total_revenue DESC
LIMIT 10;
-- Evaluates all 200 store buckets in 38ms
✓ Evaluates 200 Stores 38ms Execution
STEP 04 • CORE PRINCIPLE
The Moral: Never write repetitive queries for individual categories. GROUP BY sorts records into distinct partition piles automatically, and HAVING filters the calculated group summaries.

Concept Bridge: From Whole-Table Funnels to Categorical Piles

Understanding the structural transformation when SQL partitions data.

Pillar 01
Input Partitioning
The database engine sorts incoming rows into distinct bins sharing identical grouping keys (e.g. order_status or store_id).
Pillar 02
Independent Aggregations
Aggregate functions (COUNT, SUM, AVG) execute independently inside each partition pile, emitting exactly 1 summary row per pile.
Pillar 03
Group Filtering with HAVING
After groups and aggregates are computed, HAVING inspects the summary metrics and discards groups that do not meet threshold criteria.
retailmart • Basic Categorical Grouping Baseline
SELECT
  order_status,
  COUNT(*) AS order_count,
  ROUND(SUM(net_total), 2) AS status_revenue
FROM sales.orders
GROUP BY order_status
ORDER BY order_count DESC
LIMIT 10;

Visual Studio: The Sorting Piles Concept

Click partition columns to watch 1,50,000 orders sorted into discrete category piles.

PARTITION BY COLUMN:
INPUT TABLE: 1,50,000 SALES ORDERS
Delivered
82,540 orders
Cancelled
7,587 orders
Processing
14,951 orders
GROUP BY order_status splits 1,50,000 order rows into 7 distinct piles. Aggregates run on each pile independently!

GROUP BY: Categorical Aggregation & The Golden Rule

Dividing rows into partitions and computing independent metrics per partition.

Pillar 01
Partition Reduction
Rows sharing identical grouping keys collapse into a single output row. If there are 7 order statuses, exactly 7 rows are returned.
THE GOLDEN RULE
Select List Alignment
Every column in SELECT that is NOT wrapped inside an aggregate function MUST appear in the GROUP BY clause!
Pillar 03
Multi-Column Tuples
GROUP BY store_id, order_status partitions by unique pairs. 200 stores × 7 statuses produces up to 1,400 category rows.
retailmart • Orders Per Status Breakdown
SELECT
  order_status,
  COUNT(*) AS order_count,
  ROUND(SUM(net_total), 2) AS total_revenue
FROM sales.orders
GROUP BY order_status
ORDER BY order_count DESC
LIMIT 10;

Visual Studio: Custom Segmentation & Time-Series Bucketing

Click test buttons to trace dynamic CASE tier bucketing and monthly cohort time-series.

TEST GROUPING MODE:
CASE EXPRESSION GROUPING
High: 12k • Medium: 48k • Small: 90k
GROUP BY CASE ... END creates custom business tiers on the fly without database schema migrations.
Pattern 01
CASE Expression Grouping
GROUP BY CASE WHEN net_total > 50000 ...
Dynamically segments transactions into High, Medium, and Budget tiers without altering underlying schema.
Pattern 02
DATE_TRUNC Monthly Cohorts
GROUP BY DATE_TRUNC('month', order_date)
Truncates timestamps to calendar months (2024-01-01), enabling clean monthly revenue trend lines.
Pattern 03
Presentation Formatting
TO_CHAR(order_date, 'Mon YYYY')
Pair date truncation for chronological sorting with TO_CHAR for friendly presentation labels like 'Jan 2024'.

Time-Series & Expressions: Dynamic Grouping Buckets

Constructing monthly revenue pacing reports and custom transaction segmentation.

Pillar 01
Expression Duplication
Whatever expression appears unaggregated in SELECT must appear verbatim in GROUP BY (e.g. DATE_TRUNC('month', order_date)).
Pillar 02
Chronological Sort
Sort by the raw date value (ORDER BY sales_month), NOT the formatted text label TO_CHAR (which would sort alphabetically: Apr, Aug, Dec, Feb).
Pillar 03
Date Casting
Casting ::DATE on the output eliminates distracting time zone timestamps like 00:00:00+05:30 from reports.
retailmart • Monthly Revenue Pacing Trend
SELECT
  DATE_TRUNC('month', order_date)::DATE AS sales_month,
  TO_CHAR(order_date, 'Mon YYYY') AS month_label,
  COUNT(*) AS orders_processed,
  ROUND(SUM(net_total), 2) AS gross_revenue
FROM sales.orders
GROUP BY
  DATE_TRUNC('month', order_date),
  TO_CHAR(order_date, 'Mon YYYY')
ORDER BY sales_month
LIMIT 24;

Visual Studio: The Bouncer vs The Judge

Click execution stages below to observe where row filters and group filters operate.

EXECUTION STAGE:
STAGE 1 • PRE-AGGREGATION
WHERE (The Bouncer)
Filters raw rows BEFORE grouping. Discards cancelled orders immediately.
STAGE 2 • PARTITIONING
GROUP BY (The Buckets)
Segregates surviving delivered rows into store piles.
STAGE 3 • POST-AGGREGATION
HAVING (The Judge)
Evaluates aggregate sums. Discards stores under ₹6 crore.
Stage 1: WHERE (The Bouncer at the Door)
Filters raw individual rows BEFORE grouping. order_status = "Delivered" discards cancelled and returned orders immediately.

HAVING Clause: Post-Aggregation Group Filtering

Eliminating summarized partition groups based on aggregate threshold values.

Pillar 01
Aggregate Predicates
HAVING is designed for aggregate conditions: HAVING COUNT(*) >= 500, HAVING SUM(net_total) > 60000000.
Pillar 02
Performance Synergy
Put row conditions in WHERE (reduces rows grouped) and group conditions in HAVING. Putting row conditions in HAVING hurts query speed!
Pillar 03
Alias Portability
In standard ANSI SQL, HAVING runs before SELECT and cannot reference aliases. Always repeat the full aggregate expression in HAVING.
retailmart • High-Volume Flagship Stores
SELECT
  store_id,
  COUNT(*) AS order_count,
  ROUND(SUM(net_total), 2) AS total_revenue
FROM sales.orders
GROUP BY store_id
HAVING COUNT(*) >= 500
ORDER BY total_revenue DESC
LIMIT 15;

Visual Studio: The Physical Execution Engine Pipeline

Click each stage of query execution to understand what the database engine is computing.

PIPELINE:
STEP 1
FROM & JOIN
Identifies source tables, loads working dataset, and forms the Cartesian or joined working table.
Key Takeaway: No filtering has occurred yet. All rows from source tables are loaded.

Query Execution Order: Why SQL Rules Exist

Understanding the physical lifecycle resolves every alias and grouping error.

Pillar 01
Why WHERE Can't Use Aliases
WHERE runs in Step 2. SELECT (which creates aliases) runs in Step 5. You cannot use a column alias before it is born!
Pillar 02
Why WHERE Can't Use Aggregates
WHERE runs in Step 2. GROUP BY (which computes aggregates) runs in Step 3. Aggregates don't exist yet at the WHERE stage!
Pillar 03
Why ORDER BY CAN Use Aliases
ORDER BY runs in Step 7 — AFTER SELECT (Step 5). This is why sorting by computed column aliases is 100% legal!
retailmart • The Complete 8-Stage Pipeline
SELECT
  DATE_TRUNC('month', order_date)::DATE AS month,
  COUNT(*) AS total_orders,
  ROUND(SUM(net_total), 2) AS revenue
FROM sales.orders
WHERE order_status = 'Delivered'
GROUP BY DATE_TRUNC('month', order_date)
HAVING COUNT(*) >= 1000
ORDER BY month
LIMIT 24;

Common Mistakes: Grouping & Filtering Pitfalls

Three architectural misunderstandings that cause query compiler crashes.

Mistake 01
The Golden Rule Violation
SELECT store_id, order_status, COUNT(*) ... GROUP BY store_id
order_status is unaggregated in SELECT but omitted from GROUP BY. Fix: Either add order_status to GROUP BY, wrap it in an aggregate, or remove it from SELECT.
Mistake 02
WHERE vs HAVING Confusion
WHERE COUNT(*) > 5 • Crashes Engine!
WHERE runs before grouping and cannot see COUNT. Conversely, placing order_status = 'Delivered' in HAVING forces grouping on lakhs of doomed rows.
Mistake 03
SELECT Alias in HAVING
SELECT COUNT(*) AS cnt ... HAVING cnt > 100
HAVING runs before SELECT assigns aliases. In standard ANSI SQL this fails. Fix: Always repeat the full expression: HAVING COUNT(*) > 100.
65%
🧠Practice Time

Interview Studio: Master GROUP BY & HAVING Questions

Five core SQL grouping and query engine architecture questions interviewers ask.

Q1 WHERE vs HAVING
Q2 The Golden Rule
Q3 8-Stage Execution Order
Q4 CASE Grouping Tiers
Q5 Monthly Cohort Trends
Q1 · Filtering Architecture
What is the fundamental difference between WHERE and HAVING, and can you use both in the same query?
Answer to give:
WHERE filters individual rows before aggregation occurs. It cannot evaluate aggregate metrics like COUNT(*) because grouping has not taken place.

HAVING filters groups after GROUP BY has collapsed rows into partitions. It is designed specifically for aggregate conditions (HAVING SUM(net_total) > 60000000).

Yes, you can and should use both: Use WHERE to discard irrelevant rows early (reducing CPU and memory), and use HAVING strictly to filter aggregated summary results.

Self-Check Studio: 6 Grouping Retention Cards

Click any flashcard to flip and test your understanding of PostgreSQL grouping rules.

CLICK TO FLIP • SELF-CHECK
What is the physical execution position of HAVING relative to WHERE and SELECT?
ANSWER: Step 4: After WHERE (Step 2) and GROUP BY (Step 3), but BEFORE SELECT (Step 5).
CLICK TO FLIP • SELF-CHECK
Why does writing WHERE COUNT(*) >= 10 crash with a syntax error?
ANSWER: WHERE runs before grouping. Aggregate functions do not exist at Step 2; use HAVING.
CLICK TO FLIP • SELF-CHECK
Can GROUP BY operate on multiple columns simultaneously?
ANSWER: Yes! GROUP BY store_id, order_status creates a partition for every unique combination of values.
CLICK TO FLIP • SELF-CHECK
What happens if you sort by TO_CHAR(order_date, 'Mon YYYY') instead of DATE_TRUNC?
ANSWER: Months sort alphabetically (Apr, Aug, Dec, Feb...) instead of chronologically!
CLICK TO FLIP • SELF-CHECK
Can you use column aliases defined in SELECT inside the HAVING clause in standard SQL?
ANSWER: No. HAVING runs in Step 4 before SELECT runs in Step 5. Repeat the aggregate expression.
CLICK TO FLIP • SELF-CHECK
If a table has 100 rows and 5 distinct categories, how many rows does GROUP BY emit?
ANSWER: Exactly 5 rows — one summary row per distinct category group.

Key Takeaways: The 4 Grouping Laws

Core rules matching today's 4 curriculum pillars for production SQL analytics.

01
The Golden Rule of Grouping
Every non-aggregated column in SELECT must be explicitly declared in GROUP BY. Grouping on multiple columns partitions by unique value tuples.
02
Dynamic Bucketing & Cohorts
Group by CASE expressions to create custom tiers on the fly. Group by DATE_TRUNC('month', dt) for chronological monthly trend pacing.
03
HAVING Post-Aggregation Filtering
WHERE filters rows before grouping; HAVING filters groups after aggregation. Place row filters in WHERE and aggregate thresholds in HAVING for optimal speed.
04
The 8-Stage Execution Lifecycle
FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT. Understanding stage order resolves all alias and grouping compiler errors.

Quick Reference: GROUP BY & HAVING Cheatsheet

Production SQL syntax reference for categorical partitioning, filtering, and time-series.

GROUPING BLUEPRINTS
Basic Categorical GROUP BY▶ Run
SELECT order_status, COUNT(*)
FROM sales.orders
GROUP BY order_status
ORDER BY COUNT(*) DESC LIMIT 10;
Multi-Column Grouping▶ Run
SELECT store_id, order_status, COUNT(*)
FROM sales.orders
GROUP BY store_id, order_status LIMIT 15;
DATE_TRUNC Time-Series▶ Run
SELECT DATE_TRUNC('month', order_date)::DATE AS m, COUNT(*)
FROM sales.orders
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY m LIMIT 12;
Dynamic CASE Tiers▶ Run
SELECT
  CASE WHEN net_total > 50000 THEN 'High' ELSE 'Normal' END AS tier,
  COUNT(*)
FROM sales.orders
GROUP BY CASE WHEN net_total > 50000 THEN 'High' ELSE 'Normal' END
LIMIT 10;
HAVING & LIFECYCLE
HAVING Volume Threshold▶ Run
SELECT store_id, COUNT(*)
FROM sales.orders
GROUP BY store_id
HAVING COUNT(*) >= 500
ORDER BY COUNT(*) DESC LIMIT 15;
WHERE + HAVING Combo▶ Run
SELECT role, COUNT(*)
FROM stores.employees
WHERE salary >= 25000
GROUP BY role
HAVING COUNT(*) >= 5 LIMIT 15;
HAVING Revenue Threshold▶ Run
SELECT store_id, ROUND(SUM(net_total), 2) AS rev
FROM sales.orders
GROUP BY store_id
HAVING SUM(net_total) > 60000000
ORDER BY rev DESC LIMIT 15;
Complete 8-Stage Query▶ Run
SELECT DATE_TRUNC('month', order_date)::DATE AS m, COUNT(*)
FROM sales.orders
WHERE order_status = 'Delivered'
GROUP BY m
HAVING COUNT(*) > 1000
ORDER BY m LIMIT 10;

Take-Home Challenge: 6 Queries on RetailMart

Apply today's GROUP BY and HAVING techniques in the playground. Every task has verified non-zero results.

01
City Store Footprint
From stores.stores, group by city and display store count per city. Prayagraj leads with 8 stores, Noida has 7. Sort by store count descending, 15 rows.
02
Payment Mode Adoption
From sales.payments, group by payment_mode to report transaction count and total volume across all 9 payment channels. Cash leads with ₹131.8 crore.
03
Monthly Delivered Pacing
From sales.orders, for order_status = 'Delivered', group by DATE_TRUNC('month', order_date)::DATE to report monthly delivered volume and revenue. 24 rows.
04
High-Revenue Flagship Outlets
From sales.orders, group by store_id and filter stores where total revenue exceeds ₹6 crore (HAVING SUM(net_total) > 60000000). Sort by revenue descending, 15 rows.
05
Department Payroll Threshold
From stores.employees, group by dept_id, filter departments with ≥ 50 staff using HAVING, and report headcount, average salary, and monthly payroll.
06
Brand Pricing Spectrum
From products.products, group by brand_id, filter brands with ≥ 10 catalog SKUs using HAVING, and report SKU count, min price, max price, and average price.
✓
Ready to Execute?
Run each challenge in the interactive playground. Verify your results against real PostgreSQL data.

Your SQL Journey: Session 14 Completed

Track your progress across the complete 59-session curriculum.

MODULE 2 · S09–S12 Functions, CASE & NULLs COMPLETED ✓
MODULE 2 · S13 Aggregate Functions COMPLETED ✓
MODULE 2 · S14 GROUP BY & HAVING YOU ARE HERE ●
MODULE 2 · S15 Subtotals: ROLLUP & GROUPING() UP NEXT →

Coming Next: Subtotals: ROLLUP & GROUPING()

Expanding multi-level subtotals and hierarchical grouping.

SESSION 15 PREVIEW
Hierarchical ROLLUP Subtotals
Instead of running separate queries for grand totals, state totals, and city subtotals, ROLLUP computes multi-level subtotal hierarchies in a single execution pass!
THE GROUPING() FUNCTION
Distinguishing Subtotal NULLs
Master the GROUPING() bit-mask function to distinguish between real data NULLs and super-aggregate subtotal summary rows generated by the engine.
MODULE 3 PREVIEW
Relational JOINs Architecture
Right now orders and customers live in separate tables. Soon you connect tables using shared foreign keys: INNER, LEFT, RIGHT, and FULL OUTER JOINs!
Subtotals: ROLLUP on Live Schedule → Review From Beginning ↺
→ next   ← back   F fullscreen   H hide bars   N notes
1 / 41