Two plan shapes are worth reporting and are currently ignored.
Sort without an index. A Sort node under a Seq Scan means PostgreSQL read the table and then sorted it in memory (or spilled to disk). An index on the ORDER BY columns removes both steps. This is very common with ORDER BY created_at DESC LIMIT 20 — the pagination query on almost every list endpoint.
Hash Join on an unindexed foreign key. A join whose inner side is a sequential scan usually means the foreign key column has no index.
Where to look
src/plan.ts — walk() already traverses the tree; Sort Key is typed
src/analyze.ts — analyzePlan currently matches only Node Type === "Seq Scan" with a Filter
test/analyze.test.ts — fixtures make this easy to test without a database
Acceptance
Watch out for
A sort of 20 rows is not worth an index. Whatever threshold you choose, apply it to the rows being sorted, not the rows returned — LIMIT 20 after sorting 60,000 rows is precisely the case worth reporting, and looking at the wrong number would filter it out. That exact mistake already happened once in this codebase; see the note about Plan Rows in src/analyze.ts.
Two plan shapes are worth reporting and are currently ignored.
Sort without an index. A
Sortnode under aSeq Scanmeans PostgreSQL read the table and then sorted it in memory (or spilled to disk). An index on theORDER BYcolumns removes both steps. This is very common withORDER BY created_at DESC LIMIT 20— the pagination query on almost every list endpoint.Hash Join on an unindexed foreign key. A join whose inner side is a sequential scan usually means the foreign key column has no index.
Where to look
src/plan.ts—walk()already traverses the tree;Sort Keyis typedsrc/analyze.ts—analyzePlancurrently matches onlyNode Type === "Seq Scan"with aFiltertest/analyze.test.ts— fixtures make this easy to test without a databaseAcceptance
Suggestion.reasondistinguishes the cases (it is already a union with one member)ORDER BYsuggestions respect direction —ORDER BY a DESC, b ASCneeds(a DESC, b ASC), and getting this wrong produces an index the planner will not useWatch out for
A sort of 20 rows is not worth an index. Whatever threshold you choose, apply it to the rows being sorted, not the rows returned —
LIMIT 20after sorting 60,000 rows is precisely the case worth reporting, and looking at the wrong number would filter it out. That exact mistake already happened once in this codebase; see the note aboutPlan Rowsinsrc/analyze.ts.