EXPLAIN ANALYZE reports what happened

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

-> Table scan on <temporary>  (actual time=412..419 rows=8814 loops=1)
    -> Nested loop inner join  (cost=88104 rows=41)
       (actual time=0.4..380 rows=412008 loops=1)

-- estimated 41. actually 412,008.

The gap between rows=41 and actual rows=412008 is the whole diagnostic, and it is invisible in plain EXPLAIN. A wrong estimate usually means stale statistics or a correlation the optimiser cannot see between two predicates, and the fix is ANALYZE TABLE or a histogram before it is an index. The one caution is that EXPLAIN ANALYZE actually runs the query, so it is not something to point at a DELETE.