A common table expression reads like a named intermediate result, and in MySQL 8.0 it may be merged into the outer query or materialised into a temporary table — the optimiser decides, and the two perform very differently.
WITH recent AS (
SELECT customer_id, SUM(total) AS spent
FROM orders WHERE placed_at > '2018-01-01'
GROUP BY customer_id
)
SELECT c.name, r.spent
FROM recent r JOIN customers c ON c.id = r.customer_id
WHERE r.spent > 10000;
-- EXPLAIN will show <derived2> if it materialised
A CTE referenced once is usually merged, which is what you want. Referenced twice, it is materialised once rather than evaluated twice — which is the actual performance argument for using one over a repeated subquery. The readability is the main reason to reach for it, and the honest caveat is that a materialised CTE has no indexes, so joining a large one to another large table can be dramatically slower than a subquery the optimiser could push a predicate into.