Why can a HAVING clause refer to a SELECT alias when a WHERE clause cannot?
Because of when each clause runs. The logical order of evaluation is FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. The alias you define in the SELECT list does not exist until the SELECT stage, so WHERE — which runs before it — has nothing to refer to. HAVING runs after GROUP BY but before SELECT, yet it can still reference the alias, because most engines resolve aliases in HAVING and ORDER BY by looking ahead to the projection list rather than requiring the value to be physically materialised at that point.
The exception that proves the rule is ORDER BY, which also runs after the projection and can use the alias directly. WHERE and JOIN ON are the two clauses that genuinely cannot, because they run before the projection exists. GROUP BY is an interesting middle case: most engines allow an alias there too, but it is safer to repeat the expression or use an ordinal position, because the standard does not require alias support in GROUP BY and some engines reject it.
This is why the classic workaround is to wrap the query. If you want to filter on a computed column, compute it in a CTE or a derived table, then filter on the alias in the outer query — that works in WHERE because by then the alias is a real column of the derived relation.
One more trap: if the alias name shadows a real column name, the engine usually prefers the column, and the reference silently means something other than what you expected. That is another good reason to name derived columns distinctly from your base columns.