EXPLAIN ANALYZE shows what happened, not what might

EXPLAIN reports 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 > '2019-01-01' GROUP BY c.id;

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

The gap between rows= and actual rows= is the diagnosis: a large discrepancy means the statistics are stale, or the predicate is one 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 that happens to be expressed as SQL.