What makes a subquery correlated, and why does that matter for performance?
A subquery is correlated when it refers to a column belonging to the enclosing query. That reference creates a dependency: the subquery cannot be evaluated on its own, so the engine has to evaluate it once for each row of the outer query.
The contrast is with an uncorrelated subquery. WHERE id IN (SELECT id FROM categories WHERE name = 'peripherals') has no reference to the outer table, so the engine can run it once, get a list of four ids, and test each product against that list. A correlated subquery has no such shortcut — the inner query is genuinely different for every outer row.
The cost therefore scales with the outer row count multiplied by the inner work. Ten thousand customers each triggering a scan of the orders table is ten thousand scans, and no amount of clever planning removes that if the subquery genuinely depends on the outer row.
There are two ways to make a correlated subquery cheaper. The first is decorrelation: rewrite it as a join with a GROUP BY, which lets the engine compute the aggregate once per group and then join. SELECT c.name, SUM(o.amount) FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id does the same job as the correlated version, and most optimizers will transform one into the other automatically when they can. The second is to make the inner lookup cheap, with an index on the correlated column, so each of the ten thousand executions is a seek rather than a scan.
One related trap: a correlated subquery in the WHERE clause that returns many rows behaves like a nested loop and can be dramatically slower than the equivalent EXISTS. A correlated subquery in the SELECT list is usually fine, because it returns one value per outer row and each execution is a single indexed lookup.