The one thing to understand first
The PostgreSQL executor runs a tree of <a class="sev1-termlink" href="https://thesev1database.com/glossary/plan-tree/" title="Plan tree">plan nodes, each pulling rows from its children on demand (the Volcano/<a class="sev1-termlink" href="https://thesev1database.com/glossary/demand-pull-volcano-model/" title="Demand-pull execution (Volcano model)">iterator model, src/backend/executor/). EXPLAIN prints that tree. The node at the top produces the final result; leaves are scans of tables or indexes. Reading a plan means reading this tree from the inside out.
A bad plan is almost never a dumb planner — it is a planner working from a wrong row estimate. So the one skill that matters when reading EXPLAIN is comparing the planner’s estimated rows to the actual rows; the biggest mismatch is where your problem lives.
The cost numbers
Each node shows cost=startup..total rows=N width=B: