fix(db): add missing indexes and drop redundant ones (#319)

Postgres advisor flagged seq-scans and redundant indexes across both
backends (D1 + Postgres):

- add projects(organization_id) — org-scoped project listings seq-scanned
- add account(account_id, provider_id) — better-auth sign-in lookup
- add verification(expires_at) — expired-token cleanup range scan
- drop saved_keyword_tag_assignments_keyword_idx — covered by unique
  (saved_keyword_id, tag_id) prefix
- drop rank_snapshots_run_idx — covered by unique
  (run_id, tracking_keyword_id, device) prefix

Mirrored in both schema dialects + parity-test required-index guard.
This commit is contained in:
Ben Senescu 2026-06-30 17:11:49 -04:00 committed by Ben Senescu
parent e5e5029e6b
commit 116719a019
15 changed files with 11196 additions and 8 deletions

View File

@ -0,0 +1,3 @@
CREATE INDEX "projects_organization_id_idx" ON "projects" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "account_accountId_providerId_idx" ON "account" USING btree ("account_id","provider_id");--> statement-breakpoint
CREATE INDEX "verification_expiresAt_idx" ON "verification" USING btree ("expires_at");

View File

@ -0,0 +1,2 @@
DROP INDEX "rank_snapshots_run_idx";--> statement-breakpoint
DROP INDEX "saved_keyword_tag_assignments_keyword_idx";

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,20 @@
"when": 1781902417326, "when": 1781902417326,
"tag": "0002_clean_moira_mactaggert", "tag": "0002_clean_moira_mactaggert",
"breakpoints": true "breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1782833725049,
"tag": "0003_sturdy_may_parker",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1782851683039,
"tag": "0004_dashing_betty_ross",
"breakpoints": true
} }
] ]
} }

View File

@ -0,0 +1,3 @@
CREATE INDEX `projects_organization_id_idx` ON `projects` (`organization_id`);--> statement-breakpoint
CREATE INDEX `account_accountId_providerId_idx` ON `account` (`account_id`,`provider_id`);--> statement-breakpoint
CREATE INDEX `verification_expiresAt_idx` ON `verification` (`expires_at`);

View File

@ -0,0 +1,2 @@
DROP INDEX `rank_snapshots_run_idx`;--> statement-breakpoint
DROP INDEX `saved_keyword_tag_assignments_keyword_idx`;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -183,6 +183,20 @@
"when": 1781892941214, "when": 1781892941214,
"tag": "0025_loving_mojo", "tag": "0025_loving_mojo",
"breakpoints": true "breakpoints": true
},
{
"idx": 26,
"version": "6",
"when": 1782833723883,
"tag": "0026_wide_red_skull",
"breakpoints": true
},
{
"idx": 27,
"version": "6",
"when": 1782851682143,
"tag": "0027_reflective_molten_man",
"breakpoints": true
} }
] ]
} }

View File

@ -71,6 +71,11 @@ export const projects = sqliteTable(
.where( .where(
sql`${table.name} = 'Default' AND ${table.domain} IS NULL AND ${table.archivedAt} IS NULL`, sql`${table.name} = 'Default' AND ${table.domain} IS NULL AND ${table.archivedAt} IS NULL`,
), ),
// Every project listing filters by organization; the partial-unique index
// above only covers the Default-project row, so without this the org-scoped
// list queries seq-scan. Per-org row counts are small, so the archived/
// created_at ordering sorts cheaply on top of this single-column lookup.
index("projects_organization_id_idx").on(table.organizationId),
], ],
); );
@ -149,7 +154,8 @@ export const savedKeywordTagAssignments = sqliteTable(
table.savedKeywordId, table.savedKeywordId,
table.tagId, table.tagId,
), ),
index("saved_keyword_tag_assignments_keyword_idx").on(table.savedKeywordId), // No standalone index on savedKeywordId — the unique index above has it as
// its leftmost column, so it already serves savedKeywordId lookups.
index("saved_keyword_tag_assignments_tag_idx").on(table.tagId), index("saved_keyword_tag_assignments_tag_idx").on(table.tagId),
], ],
); );
@ -323,7 +329,8 @@ export const rankSnapshots = sqliteTable(
.default(sql`(current_timestamp)`), .default(sql`(current_timestamp)`),
}, },
(table) => [ (table) => [
index("rank_snapshots_run_idx").on(table.runId), // No standalone index on runId — the unique index below has it as its
// leftmost column, so it already serves runId lookups.
index("rank_snapshots_keyword_device_idx").on( index("rank_snapshots_keyword_device_idx").on(
table.trackingKeywordId, table.trackingKeywordId,
table.device, table.device,

View File

@ -74,7 +74,15 @@ export const account = sqliteTable(
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("account_userId_idx").on(table.userId)], (table) => [
index("account_userId_idx").on(table.userId),
// better-auth looks up accounts by (accountId, providerId) on every
// credential/OAuth sign-in; without this it seq-scans the account table.
index("account_accountId_providerId_idx").on(
table.accountId,
table.providerId,
),
],
); );
export const verification = sqliteTable( export const verification = sqliteTable(
@ -92,7 +100,12 @@ export const verification = sqliteTable(
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("verification_identifier_idx").on(table.identifier)], (table) => [
index("verification_identifier_idx").on(table.identifier),
// better-auth's periodic cleanup deletes rows via `expires_at < now()`,
// a range scan that seq-scans the whole table without this index.
index("verification_expiresAt_idx").on(table.expiresAt),
],
); );
export const organization = sqliteTable( export const organization = sqliteTable(

View File

@ -82,6 +82,11 @@ export const projects = pgTable(
.where( .where(
sql`${table.name} = 'Default' AND ${table.domain} IS NULL AND ${table.archivedAt} IS NULL`, sql`${table.name} = 'Default' AND ${table.domain} IS NULL AND ${table.archivedAt} IS NULL`,
), ),
// Every project listing filters by organization; the partial-unique index
// above only covers the Default-project row, so without this the org-scoped
// list queries seq-scan. Per-org row counts are small, so the archived/
// created_at ordering sorts cheaply on top of this single-column lookup.
index("projects_organization_id_idx").on(table.organizationId),
], ],
); );
@ -154,7 +159,8 @@ export const savedKeywordTagAssignments = pgTable(
table.savedKeywordId, table.savedKeywordId,
table.tagId, table.tagId,
), ),
index("saved_keyword_tag_assignments_keyword_idx").on(table.savedKeywordId), // No standalone index on savedKeywordId — the unique index above has it as
// its leftmost column, so it already serves savedKeywordId lookups.
index("saved_keyword_tag_assignments_tag_idx").on(table.tagId), index("saved_keyword_tag_assignments_tag_idx").on(table.tagId),
], ],
); );
@ -316,7 +322,8 @@ export const rankSnapshots = pgTable(
checkedAt: timestampColumn("checked_at").notNull().default(isoNow), checkedAt: timestampColumn("checked_at").notNull().default(isoNow),
}, },
(table) => [ (table) => [
index("rank_snapshots_run_idx").on(table.runId), // No standalone index on runId — the unique index below has it as its
// leftmost column, so it already serves runId lookups.
index("rank_snapshots_keyword_device_idx").on( index("rank_snapshots_keyword_device_idx").on(
table.trackingKeywordId, table.trackingKeywordId,
table.device, table.device,

View File

@ -66,7 +66,15 @@ export const account = pgTable(
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("account_userId_idx").on(table.userId)], (table) => [
index("account_userId_idx").on(table.userId),
// better-auth looks up accounts by (accountId, providerId) on every
// credential/OAuth sign-in; without this it seq-scans the account table.
index("account_accountId_providerId_idx").on(
table.accountId,
table.providerId,
),
],
); );
export const verification = pgTable( export const verification = pgTable(
@ -82,7 +90,12 @@ export const verification = pgTable(
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("verification_identifier_idx").on(table.identifier)], (table) => [
index("verification_identifier_idx").on(table.identifier),
// better-auth's periodic cleanup deletes rows via `expires_at < now()`,
// a range scan that seq-scans the whole table without this index.
index("verification_expiresAt_idx").on(table.expiresAt),
],
); );
export const organization = pgTable( export const organization = pgTable(

View File

@ -225,7 +225,9 @@ const REQUIRED_BETTER_AUTH_INDEXES: {
}[] = [ }[] = [
{ table: "session", columns: ["user_id"], unique: false }, { table: "session", columns: ["user_id"], unique: false },
{ table: "account", columns: ["user_id"], unique: false }, { table: "account", columns: ["user_id"], unique: false },
{ table: "account", columns: ["account_id", "provider_id"], unique: false },
{ table: "verification", columns: ["identifier"], unique: false }, { table: "verification", columns: ["identifier"], unique: false },
{ table: "verification", columns: ["expires_at"], unique: false },
{ table: "organization", columns: ["slug"], unique: true }, { table: "organization", columns: ["slug"], unique: true },
{ table: "member", columns: ["organization_id"], unique: false }, { table: "member", columns: ["organization_id"], unique: false },
{ table: "member", columns: ["user_id"], unique: false }, { table: "member", columns: ["user_id"], unique: false },