# Index Cleanup
Audit of `apps/cloud/drizzle/0000_lame_rage.sql` against the query
patterns in `packages/core/sdk/src/executor.ts`. Summary: mostly
over-indexed, one important miss.
## Redundant: single-column `scope_id` indexes (× 15)
Every scoped table has a composite PK `(scope_id, id)` AND a separate
`
_scope_id_idx`. Postgres composite btrees support leftmost-
prefix queries — `WHERE scope_id = ?` uses the PK directly. The
single-column indexes are dead weight: extra storage, extra write
amplification on every insert, never chosen by the planner when the
composite PK is available.
Tables affected (all drop candidates):
- `definition_scope_id_idx`
- `graphql_operation_scope_id_idx`, `graphql_source_scope_id_idx`
- `mcp_binding_scope_id_idx`, `mcp_oauth_session_scope_id_idx`, `mcp_source_scope_id_idx`
- `openapi_oauth_session_scope_id_idx`, `openapi_operation_scope_id_idx`, `openapi_source_scope_id_idx`
- `secret_scope_id_idx`
- `source_scope_id_idx`
- `tool_scope_id_idx`
- `workos_vault_metadata_scope_id_idx`
Root cause: `core-schema.ts` and plugin schemas mark `scope_id: {
index: true }`. The drizzle generator should probably _skip_ emitting
a single-column index on a column that's already the leftmost column
of a composite primary key. Fix in two places:
1. **Generator** (`packages/core/cli/src/generators/drizzle.ts`) — skip
emitting `index(...)` for `field.index` when the field is the first
column of a composite PK.
2. **Schemas** — drop `index: true` from the `scope_id` field
declarations (fallback if we want to be explicit).
Cheaper to fix at the generator level — behaves correctly regardless
of what plugin authors write.
## Missing: `memberships.organization_id`
`memberships` has PK `(account_id, organization_id)`. Queries:
- `WHERE account_id = ?` — "orgs for this user" — uses PK leftmost ✓
- `WHERE organization_id = ?` — "members of this org" — **table scan**
The second query is at least as common as the first. Fix:
```sql
CREATE INDEX "memberships_organization_id_idx"
ON "memberships" USING btree ("organization_id");
```
Or flip the PK column order, but that's a bigger migration.
## Probably dead weight — worth auditing queries
Nothing in the executor source currently queries by these fields alone.
Keep or drop based on where these are actually exercised (admin UIs?
analytics?):
- `definition_plugin_id_idx`
- `source_plugin_id_idx`
- `tool_plugin_id_idx`
- `secret_provider_idx`
If kept, leave a note on the call site so future maintainers know why.
## Fine as-is
- `*_source_id_idx` on child tables (`definition`, `mcp_binding`,
`graphql_operation`, `openapi_operation`, `tool`) — exercised by
`findMany(where: source_id = X)` via the scoped adapter.
- Composite `(scope_id, source_id)` would be marginally better than the
current `source_id` single-column index (lets the planner use one
index for both filters), but not worth the churn unless we're
redoing the DDL anyway.
## Rollout
Not blocking PR #262. Do this as a follow-up: one small PR that
(a) teaches the drizzle generator the "skip index on composite-PK-
leading column" rule, (b) regenerates the schemas, (c) adds the
`memberships_organization_id_idx`, (d) produces a single cleanup
migration. Zero code changes to the runtime.