SQL Joins Explained: Inner, Left, Right and Full
What INNER, LEFT, RIGHT and FULL joins actually do, why rows duplicate or disappear, and how to pick the right one for a report.
Short answer: Assign every user to the month they first showed up, measure how many months later each activity happened, then count distinct users per cohort and month and divide by the cohort’s starting size. The result is the familiar retention triangle.
“Are we keeping our customers?” sounds like one question. It is really many, because customers who joined in January behave differently from those who joined in June. A single retention percentage blends them all together and hides the story.
Cohort analysis fixes that. The pattern is short once you see it. The dev.to walkthrough and Holistics describe it the same way, and it is the same three steps I use.
A cohort is a group of users who share a starting point, usually the month of their first order or signup. Retention asks what share of that group is still active N periods later.
The rule that makes it fair: compare cohorts at the same age, not on the same calendar date. January’s cohort in month 3 is comparable with March’s cohort in month 3. Putting them side by side on a May dashboard is not.
One table of activity events with a user and a date. I’ll call it orders, with customer_id and order_date. The names and numbers here are illustrative.
The query below is PostgreSQL syntax. It works in DuckDB too.
Step 1: give each customer a cohort month.
WITH first_order AS (
SELECT customer_id,
DATE_TRUNC('month', MIN(order_date)) AS cohort_month
FROM orders
GROUP BY customer_id
),
Step 2: compute how many months after the cohort each activity happened.
activity AS (
SELECT DISTINCT
o.customer_id,
f.cohort_month,
(DATE_PART('year', DATE_TRUNC('month', o.order_date)) - DATE_PART('year', f.cohort_month)) * 12
+ (DATE_PART('month', DATE_TRUNC('month', o.order_date)) - DATE_PART('month', f.cohort_month))
AS months_since
FROM orders o
JOIN first_order f USING (customer_id)
),
DISTINCT matters. A customer with three orders in the same month should count once.
Step 3: count users per cohort and age, then turn the counts into percentages.
cohort_counts AS (
SELECT cohort_month, months_since, COUNT(DISTINCT customer_id) AS active_customers
FROM activity
GROUP BY cohort_month, months_since
)
SELECT
cohort_month,
months_since,
active_customers,
ROUND(
100.0 * active_customers
/ FIRST_VALUE(active_customers) OVER (PARTITION BY cohort_month ORDER BY months_since),
1
) AS retention_pct
FROM cohort_counts
ORDER BY cohort_month, months_since;
FIRST_VALUE grabs the month-0 size for each cohort, so every row is divided by the number of customers who started. This window-function trick is covered in more detail in SQL window functions.
Pivot it in your BI tool, or in SQL with CASE WHEN, and you get a triangle. One row per cohort, one column per month since joining:
| Cohort | Month 0 | Month 1 | Month 2 | Month 3 |
|---|---|---|---|---|
| Jan | 100% | 42% | 33% | 29% |
| Feb | 100% | 45% | 35% | |
| Mar | 100% | 39% |
Illustrative numbers, not real data.
Look at it in two directions:
COUNT(DISTINCT customer_id).Retention curves feed straight into forecasting. If you know how each cohort decays, you can project revenue from customers you already have, which is the idea behind cohort-based revenue forecasting for e-commerce.
My view: build the cohort table before you build any fancier churn model. It is a few dozen lines of SQL, and it usually shows you the problem faster than a model will.
Sources: DEV Community: Cohort Retention Analysis in SQL, Step by Step, Holistics: Calculate Cohort Retention Analysis with SQL. All tables and numbers are illustrative.
Part of: SQL & Local Analytics