Skip to content

Reduce dots() N+1 queries to a single batch fetch - #146

Open
OpenGithubs wants to merge 1 commit into
CopilotKit:mainfrom
OpenGithubs:perf-dots-join
Open

OpenGithubs wants to merge 1 commit into
CopilotKit:mainfrom
OpenGithubs:perf-dots-join

Conversation

@OpenGithubs

Copy link
Copy Markdown

Summary

Performance optimization: WorkspaceStore.dots() used an N+1 query pattern — one SELECT * FROM dots followed by one SELECT spaceId FROM dot_spaces WHERE dotId=? per dot. With N dots that is N+1 database round-trips just to list the workspace. Now fetches all dot_spaces rows once and groups them in memory with a Map.

Changes

  • src/server/workspace.ts: batch fetch dot_spaces, group in-memory, cut N+1 round-trips down to 2.

Commits

  • perf-dots-join perf: reduce dots() N+1 queries to a single batch fetch

Previously dots() issued one dot_spaces SELECT per dot (N+1 pattern).
Now fetches all dot_spaces rows once and groups them in memory via a Map.
With 10 dots this cuts 11 DB round-trips down to 2.
@charan-rathore

Copy link
Copy Markdown
Contributor

Verified the win against main: dots() goes from N+1 round-trips to 2, and grouping with a Map preserves the old ORDER BY spaceId since .sort() on these TEXT ids is the same lexicographic order.

One behavior change hiding in the fallback: (byDot.get(String(row.id)) ?? [row.spaceId]).sort(). For a dot with zero dot_spaces rows the old per-dot query returned [], but this returns the legacy dots.spaceId column instead. Through the app's own write paths it can't happen today: both createDot and updateDot run validateSpaceAccess, which forces the default space into spaceIds, so every dot ends up with at least one grant row. But if a row ever lands outside those paths, the fallback reports a grant the grants table doesn't have, and canAccessSpace (which reads dot_spaces directly) would deny it, so dots() and the access check would disagree. ?? [] keeps dots() a pure projection of dot_spaces. One-line change, worth making while you're in here.

@charan-rathore

Copy link
Copy Markdown
Contributor

Checked this locally before weighing in. Ran the old and new dots() against the same fixture (5 dots across 3 spaces, one dot granted to a second space): outputs are byte-identical and prepare calls drop from 6 to 2, so the N+1 to batch claim holds up.

One thing I looked at while reviewing: the ?? [row.spaceId] fallback. I don't think it can fire. The migration backfills dot_spaces from dots, createDot inserts a grant per spaceId (defaulting to the dot's own space), and validateSpaceAccess rejects spaceIds that leave out the default space, so every dot always has at least one grant row. Keeping the fallback is harmless, but a short comment noting that invariant would save the next reader the same audit.

Ordering is also equivalent: SQLite's default BINARY collation on these ASCII ids matches JS .sort().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants