fix(explore): pin the dominant-file aggregation to an edges-first plan (#2575)

The dominant-file aggregation behind codegraph_explore could let SQLite
drive the query from the nodes table, which on a large index took
seconds to a minute. The query now pins an edges-first plan. On four
indexes it returns the same rows, 4 to 12 times faster.

Fixes issue 2571.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eric Minish
2026-10-20 16:59:07 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b5553e238a
commit c8f154b9f4
3 changed files with 136 additions and 10 deletions
+24 -10
View File
@@ -242,6 +242,28 @@ function rowToFileRecord(row: FileRow): FileRecord {
};
}
/**
* Files ranked by edges between two of their own symbols — the input to
* `getDominantFile`. CROSS JOIN pins the loop order to edges first, then a
* lookup by id for each end: one pass over the edges whatever the statistics
* say. With a plain JOIN the planner may start from nodes and, for each node,
* walk every node in its file and probe edges by (source, target) — quadratic
* in nodes per file. Sampled stats (`analysis_limit` + `PRAGMA optimize`) can
* under-count nodes per file and pick that plan, and a few huge files (a
* generated R.java) then cost ~100 s (#2571). Ties go to the path, so the
* plan never decides between equal counts.
*/
export const DOMINANT_FILE_SQL = `
SELECT n.file_path AS file_path, COUNT(*) AS edge_count
FROM edges e
CROSS JOIN nodes n ON e.source = n.id
CROSS JOIN nodes m ON e.target = m.id
WHERE n.file_path = m.file_path
GROUP BY n.file_path
ORDER BY edge_count DESC, n.file_path
LIMIT 20
`;
/**
* Query builder for the knowledge graph database
*/
@@ -1169,16 +1191,8 @@ export class QueryBuilder {
// protobuf stub) outranks the real `server/etcdserver/server.go`
// (470 edges) by 4×, and the boost would push the agent toward
// generated code.
this.stmts.getDominantFile = this.db.prepare(`
SELECT n.file_path AS file_path, COUNT(*) AS edge_count
FROM edges e
JOIN nodes n ON e.source = n.id
JOIN nodes m ON e.target = m.id
WHERE n.file_path = m.file_path
GROUP BY n.file_path
ORDER BY edge_count DESC
LIMIT 20
`);
// Edges-first and tie-broken by path on purpose: see DOMINANT_FILE_SQL (#2571).
this.stmts.getDominantFile = this.db.prepare(DOMINANT_FILE_SQL);
}
const rows = this.stmts.getDominantFile.all() as Array<{ file_path: string; edge_count: number }>;
const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));