SECTION 1
Historical Context & Motivation
The practice of grouping customers or users into discrete segments for analysis long predates modern computing. In the early days of direct mail marketing, businesses manually sorted customer lists by purchase recency or geographic region to tailor their campaigns. As relational databases matured through the 1980s and 1990s, the SQL standard provided declarative tools — most notably GROUP BY and CASE expressions — that allowed analysts to perform these segmentation tasks at scale without writing procedural code. The convergence of e-commerce, SaaS subscription models, and data-driven product management in the 2000s and 2010s elevated cohort analysis from a niche marketing technique into a core competency for any data-literate engineer or product analyst.
1986 • SQL-86 Standard Ratified
1992 • CASE Expressions in SQL-92
2003 • Window Functions Emerge
2010s • Rise of Product Analytics
The central question these tools address is straightforward yet powerful: how can we assign each row in a dataset to a semantically meaningful group, and then aggregate metrics within those groups to reveal patterns that would otherwise be invisible in raw, row-level data? That is precisely what cohort and segmentation queries accomplish.
SECTION 2
Core Principles & Definitions
Before writing a single query, it is essential to internalize the foundational ideas that underpin cohort and segmentation analysis. A cohort is a group of entities — typically users — who share a common characteristic anchored to a point in time, such as the month they first registered. A segment is a broader categorization that may or may not be time-based: think "high-value customers" versus "free-tier users." While the terminology differs, the SQL machinery is largely the same: derive a label for each row, then aggregate by that label.
GROUP BY Clause
CASE Expression
Cohort Key
Segment Label
Retention Period
KEY TAKEAWAY
SECTION 3
Visual Explanation — The Cohort Pipeline
The diagram above captures the mental model you should carry when constructing any cohort or segmentation query. First, identify the raw grain of your data — typically one row per event or one row per user. Second, define the derived labels that partition rows into groups using CASE expressions, date-truncation functions, or joins to dimension tables. Third, specify the GROUP BY clause listing every non-aggregate column, which collapses potentially millions of rows into a concise summary. Finally, select the aggregate metrics — COUNT, SUM, AVG, MIN, MAX — that answer the business question. This four-step pipeline is universal across dialects: PostgreSQL, MySQL, BigQuery, Snowflake, and others.
SECTION 4
How GROUP BY and CASE Work Together
CASE Expression Syntax
The searched CASE expression evaluates Boolean predicates in order and returns the value associated with the first predicate that evaluates to TRUE. If no predicate matches, the ELSE branch fires (defaulting to NULL when ELSE is omitted). Because CASE is an expression — not a statement — it can appear anywhere a scalar value is legal: in SELECT lists, WHERE filters, ORDER BY, and even inside aggregate functions. This composability is what makes it the workhorse of segmentation logic.
SEARCHED CASE PATTERN
predicateₙ = any Boolean expression referencing table columns; valueₙ = scalar result (string, number, date); default_value = returned when no predicate is TRUE.
GROUP BY Execution Model
Conceptually, GROUP BY partitions the filtered row set (after FROM, JOIN, and WHERE) into groups where every row within a group has the same tuple of values for the listed columns. The database engine then evaluates aggregate functions once per group, producing exactly one output row per group. When you combine CASE in the SELECT list with GROUP BY on the same CASE expression (or its alias), you are effectively defining a virtual column that exists only for the purpose of aggregation — no schema change required.
GROUP BY WITH DERIVED COLUMN
f(col) = any deterministic expression (CASE, DATE_TRUNC, SUBSTRING, etc.); AGG = aggregate function; the GROUP BY must reference the same expression or its ordinal position.
Conditional Aggregation Pattern
A closely related technique places CASE inside the aggregate function rather than alongside GROUP BY. This conditional aggregation pattern pivots segments into separate columns instead of separate rows, which is ideal for building retention matrices or cross-tab reports.
CONDITIONAL AGGREGATION
Rows where the condition is FALSE produce NULL, which COUNT and SUM silently skip. This avoids the need for multiple filtered subqueries.
SECTION 5
Building a Cohort Retention Matrix
The classic deliverable of cohort analysis is the retention matrix (also called a retention triangle). Each row represents a signup cohort — for example, all users who created an account in January 2024. Each column represents a period offset from that cohort's origin: Month 0 (the signup month itself), Month 1, Month 2, and so on. The cell values typically show the percentage of the original cohort that was still active during the offset period. Constructing this matrix in SQL requires combining DATE_TRUNC for the cohort key, a date-difference calculation for the period offset, GROUP BY on both dimensions, and COUNT DISTINCT to measure returning users.
Notice the triangular shape of the matrix: more recent cohorts have fewer elapsed periods, so their later columns are blank. The period offset is computed by subtracting the cohort date from the event date, typically using DATE_DIFF or equivalent. The conditional aggregation variant uses COUNT(CASE WHEN period = 1 THEN user_id END) to pivot periods into individual columns, producing a result set that maps directly onto the visual matrix above without post-processing.
SECTION 6
Worked Example — Revenue Segmentation by Cohort
Suppose you work at an e-commerce company with a users table (columns: user_id, signup_date) and an orders table (columns: order_id, user_id, order_date, amount). The product manager asks: "For each quarterly signup cohort, show the number of users, average order value, and break them into 'power', 'regular', and 'light' spender segments based on total spend."
Revenue Segmentation by Signup Cohort
- 1
Step 1 — Compute per-user total spend
We first need each user's total lifetime spend. This requires joininguserstoordersand aggregating. We use a CTE for clarity:WITH user_spend AS ( SELECT u.user_id, u.signup_date, COALESCE(SUM(o.amount), 0) AS total_spend FROM users u LEFT JOIN orders o ON u.user_id = o.user_id GROUP BY u.user_id, u.signup_date )One row per user with their signup_date and total_spend. - 2
Step 2 — Derive cohort and segment labels
In the outer query, we derive two classification columns. The cohort is the calendar quarter of signup:DATE_TRUNC('quarter', signup_date) AS cohort_qtr. The spending segment uses a CASE expression:CASE WHEN total_spend >= 1000 THEN 'power' WHEN total_spend >= 200 THEN 'regular' ELSE 'light' END AS spend_tier.Each user row now carries a cohort_qtr label and a spend_tier label. - 3
Step 3 — Aggregate with GROUP BY
We group by both derived columns and compute aggregates:SELECT cohort_qtr, spend_tier, COUNT(*) AS user_count, ROUND(AVG(total_spend), 2) AS avg_spend FROM user_spend GROUP BY cohort_qtr, spend_tier ORDER BY cohort_qtr, spend_tier. Note that GROUP BY references the same expressions used in the SELECT list.Compact result: one row per (cohort_qtr, spend_tier) combination with user_count and avg_spend. - 4
Step 4 — Interpret sample output
Assume the query returns: Q1-2024 / power / 85 users / $1,742 avg; Q1-2024 / regular / 430 users / $487 avg; Q1-2024 / light / 685 users / $62 avg. This tells us the Q1 cohort was bottom-heavy: 57% were light spenders. Product can now investigate whether onboarding changes in Q2 shifted this distribution.The combined CTE + CASE + GROUP BY pattern answers multi-dimensional segmentation questions in a single, readable query.
SECTION 7
Strengths, Limitations & Alternatives
| Dimension | GROUP BY + CASE | Window Functions |
|---|---|---|
| Output grain | One row per group — collapses detail rows | Preserves every input row; adds columns |
| Readability | Simple, familiar to all SQL users | More complex syntax (PARTITION BY, ORDER BY) |
| Percentile / ranking | Requires self-join or subquery | Native NTILE, PERCENT_RANK functions |
| Running totals | Requires correlated subquery — slow | SUM() OVER (ORDER BY …) — efficient |
| Performance at scale | Well-optimized in all engines; hash/sort agg | Can be costly if partition is wide |
| Best for | Summary reports, dashboards, retention matrices | Row-level labeling, sessionization, funnel ordering |
GROUP BY + CASE vs. Window Functions for segmentation tasks
Common Pitfall
KEY TAKEAWAY
SECTION 8
Connection to Advanced Techniques
The GROUP BY + CASE pattern forms the foundation upon which more sophisticated analytical techniques are built. As your SQL proficiency grows, you will encounter scenarios where basic grouping is insufficient and you need to layer additional tools on top. Understanding how today's patterns extend into advanced territory will help you recognize when to level up.
| Basic Pattern | Advanced Extension | When to Upgrade |
|---|---|---|
| GROUP BY with CASE tiers | NTILE() / PERCENT_RANK() window functions | When bucket boundaries should be data-driven (percentiles) rather than hard-coded |
| Conditional aggregation for pivoting | PIVOT / CROSSTAB or dynamic SQL | When the number of pivot columns is unknown at query-write time |
| Single-level GROUP BY | GROUPING SETS / ROLLUP / CUBE | When you need subtotals, grand totals, or multiple grouping levels in one pass |
| Static cohort assignment | LAG / LEAD for churn detection | When you need to compare each user's current period to their prior period |
| Retention by period offset | Survival analysis via cumulative sums | When you need to model time-to-churn as a continuous distribution |
From basic GROUP BY + CASE to advanced analytics patterns
One particularly powerful extension is GROUPING SETS, which lets you produce multiple levels of aggregation — for example, totals by cohort only, by segment only, and by both — in a single query pass. The ROLLUP and CUBE operators are syntactic shortcuts for common GROUPING SETS patterns and are particularly useful in BI dashboards that display drill-down hierarchies. As you internalize the core GROUP BY + CASE workflow, these extensions will feel like natural generalizations rather than foreign concepts.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
orders(order_id, user_id, order_date, total), write a query that classifies each order as 'small' (total < 50), 'medium' (50 ≤ total < 200), or 'large' (total ≥ 200) and returns the count and average total for each size category.Show answer
Question 3: INTERMEDIATE
users(user_id, signup_date) and logins(login_id, user_id, login_date), write a query that computes monthly cohort retention — specifically, for each signup month, the count of distinct users who logged in during each of the first three calendar months after their signup month (period 1, 2, and 3). Return columns: cohort_month, period_1_users, period_2_users, period_3_users.Show answer
Question 4: APPLIED
users(user_id, signup_date) and sessions(session_id, user_id, session_date). Write the complete query and explain each CTE.Show answer
Question 5: CRITICAL THINKING
Show answer
SUMMARY
Lesson Summary
Summary
This lesson covered the construction of cohort and segmentation queries using two foundational SQL constructs. The CASE expression provides inline conditional logic to derive segment labels (such as spending tiers or engagement levels), while GROUP BY collapses rows sharing the same label into summary aggregates like COUNT, SUM, and AVG. When the label is a time-truncated field — such as DATE_TRUNC('month', signup_date) — the groups become temporal cohorts suitable for retention analysis.
We explored the conditional aggregation technique — placing CASE inside an aggregate function — to pivot retention periods or segment metrics into columns. We examined how CTEs improve readability by isolating the per-user pre-aggregation step from the cohort-level summary. The trade-off between hard-coded thresholds and data-driven percentile boundaries was discussed, along with forward-looking extensions such as GROUPING SETS, ROLLUP, and window-function-based approaches like NTILE and LAG.