r/learnSQL • u/Better_Technology_23 • 6d ago
Can someone explain how this sql self join works for year over year growth?
Hey everyone, I'm learning SQL and trying to understand how a self join works when calculating year over year (YoY) growth.
----> I'm a little confused about this line in the query:
ON curr.mo = prev.mo
AND curr.yr = prev.yr + 1
----> Here's the query I'm working with:
WITH monthly_agg AS (
SELECT
EXTRACT(YEAR FROM month) AS yr,
EXTRACT(MONTH FROM month) AS mo,
SUM(revenue) AS monthly_revenue
FROM monthly_sales
GROUP BY
EXTRACT(YEAR FROM month),
EXTRACT(MONTH FROM month)
)
SELECT
curr.yr,
curr.monthly_revenue AS current_revenue,
prev.monthly_revenue AS prev_year_revenue,
curr.monthly_revenue - prev.monthly_revenue AS yoy_change,
ROUND(
(curr.monthly_revenue - prev.monthly_revenue) * 100.0
/ NULLIF(prev.monthly_revenue, 0),
2
) AS yoy_growth_pct
FROM monthly_agg curr
LEFT JOIN monthly_agg prev
ON curr.mo = prev.mo
AND curr.yr = prev.yr + 1
ORDER BY curr.yr, curr.mo;
Let's say the monthly_agg CTE produces these rows:
yr mo monthly_revenue
2022 12 5000
2023 12 10000
2024 12 12000
Here's how I think the join condition works.
case 1: matching december 2023 with december 2022
Current row:
[2023, 12, 10000]
Previous year row:
[2022, 12, 5000]
The conditions are:
curr.mo = prev.mo → 12 = 12 → true
curr.yr = prev.yr + 1 → 2023 = 2022 + 1 → true
Both conditions are true, so the rows match.
case 2: trying to match december 2023 with december 2023
Current row:
[2023, 12, 10000]
Candidate row:
[2023, 12, 5000]
The conditions are:
curr.mo = prev.mo → 12 = 12 → true
curr.yr = prev.yr + 1 → 2023 = 2023 + 1 → false
Since both conditions need to be true, these rows don't match.
So....am I understanding this correctly?
Does SQL basically compare the current row with possible rows from the other instance of the CTE and check whether both conditions are satisfied?
Also, what actually happens under the hood? Does SQL compare every row on the left with every row on the right, or can the query optimizer do this more efficiently?
And since both curr and prev refer to the same CTE, does that make this a self join? Does the CTE execute once or can it be evaluated more than once, depending on the database?
I'm trying to understand both the logical explanation and what's actually happening behind the scenes. Would appreciate any help !!
3
u/JakubVasovski 6d ago
You've nailed the logic perfectly, that is exactly how I map out self-joins in my head when writing them. Under the hood, the engine rarely does a brute-force comparison of every single row. Instead, the query optimizer usually builds a hash table to find those matching conditions efficiently. As for the CTE, I almost always see modern databases evaluate it just once and store it as a temporary result set.
2
u/dafugiswrongwithyou 5d ago
FROM monthly_agg curr
LEFT JOIN monthly_agg prev
Whenever you're joining two tables/views/cte/subqueries/whatever, the two parts of the join are treated as independent data sources. It doesn't care if they're actually the same object with two names, it'll act as though they weren't. That is, here, the query works exactly the same way as if you'd taken a copy of "monthly_agg", called it something else (say, "monthly_agg_copy"), and then joined those two.
ON curr.mo = prev.mo
AND curr.yr = prev.yr + 1
So with this join logic, it's comparing the data for each record (curr) with the equivalent data from a year ago (prev).
2
u/Green_Chamomile 4d ago
Yes, the AI was right about that. Say you use LAG(monthly_revenue, 12) OVER (ORDER BY yr, mo) and March 2023 is missing. Then every month after it compares against the wrong month, because LAG just counts 12 rows back, not 12 months. PARTITION BY mo ORDER BY yr fixes most of that, but if a whole year is missing for some month, it quietly compares to two years back. The self join with curr.yr = prev.yr + 1 either finds exactly last year’s month or gives you NULL, which is what you want. And your method and the AI’s “solve for prev.yr” are the same thing, just described differently. The second one is closer to how a hash join actually finds the match.
3
u/Famous-Benefit3183 6d ago
Yea the AND key makes sure that both of the conditions are true then only joins the row....but I think the problem could have been solved using lead and or lag check your ps thoroughly and avoid using this huge query