A self join is just a regular join where a table is joined to itself — useful whenever a comparison needs to happen between two rows of the same table, such as finding pairs of employees, not employees compared to some other table. There's nothing structurally special about it: you alias the same table twice and join those aliases like any other two tables.
-- For every employee, find every colleague in the same department who earns less
SELECT e1.name AS employee, e2.name AS earns_more_than, e1.dept
FROM Employees e1
JOIN Employees e2 ON e1.dept = e2.dept AND e1.salary > e2.salary;
-- Bob | Alice | Sales (Bob earns more than Alice, same dept)
-- Carol | Dave | IT -- only if Dave's salary weren't NULL; NULL > anything is UNKNOWN, so this pair never matches
That last comment matters: because e1.salary > e2.salary involves NULL whenever Dave is on either side, Dave never appears as either party in this join's output — not because Dave was "compared and lost," but because a comparison against NULL doesn't evaluate to true or false, so the join condition simply never matches for him. This is a preview of the three-valued logic covered in the next few chapters.
This example is also a non-equi join — the join condition uses > rather than =. Nothing about JOIN ... ON requires the condition to be equality; any boolean expression comparing the two sides works, including range comparisons (BETWEEN, <, >=), which show up constantly in real schemas for things like "which pricing tier does this order amount fall into" or "which shift was this timestamp during."