EXPLAIN ANALYZE reports what happened, not what might

EXPLAIN shows the plan and the optimiser’s estimates, and the estimates are frequently wrong — a query planned for 40 rows that reads 400,000 looks entirely healthy.

EXPLAIN ANALYZE
SELECT c.name, COUNT(*) FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.placed_at > '2020-01-01' GROUP BY c.id;

-- -> Nested loop inner join  (cost=8241 rows=8100)
--      (actual time=0.09..412 rows=94012 loops=1)
--                                   ^^^^^^^^^^^ estimate was 8,100

The gap between rows= and actual rows= is the diagnosis: a large discrepancy means stale statistics or a predicate the optimiser cannot estimate, and both have known fixes. It runs the query, so it is not something to point at a DELETE. The loops= figure is the other one to read — a nested loop with a high loop count is the plan-level signature of an N+1 expressed as SQL.