perf(db): otimizar índices — remover 7 sem uso, adicionar 17 em FKs

Baseado em análise do pg_stat_user_indexes (187 dias de estatísticas):
removidos 7 índices com 0 scans e adicionados 17 índices em foreign
keys que antes geravam sequential scans durante deletes nas tabelas pai.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-16 21:16:26 +00:00
parent 5635705c56
commit 4d9a1c0a35
4 changed files with 3097 additions and 148 deletions

View File

@@ -0,0 +1,24 @@
DROP INDEX "tokens_api_user_id_idx";--> statement-breakpoint
DROP INDEX "cartoes_user_id_status_idx";--> statement-breakpoint
DROP INDEX "dashboard_notification_states_user_id_archived_idx";--> statement-breakpoint
DROP INDEX "contas_user_id_status_idx";--> statement-breakpoint
DROP INDEX "antecipacoes_parcelas_series_id_idx";--> statement-breakpoint
DROP INDEX "pagadores_user_id_status_idx";--> statement-breakpoint
DROP INDEX "pagadores_user_id_role_idx";--> statement-breakpoint
CREATE INDEX "account_user_id_idx" ON "account" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "anexos_user_id_idx" ON "anexos" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "orcamentos_categoria_id_idx" ON "orcamentos" USING btree ("categoria_id");--> statement-breakpoint
CREATE INDEX "cartoes_conta_id_idx" ON "cartoes" USING btree ("conta_id");--> statement-breakpoint
CREATE INDEX "import_category_mappings_category_id_idx" ON "import_category_mappings" USING btree ("category_id");--> statement-breakpoint
CREATE INDEX "pre_lancamentos_lancamento_id_idx" ON "pre_lancamentos" USING btree ("lancamento_id");--> statement-breakpoint
CREATE INDEX "antecipacoes_parcelas_lancamento_id_idx" ON "antecipacoes_parcelas" USING btree ("lancamento_id");--> statement-breakpoint
CREATE INDEX "antecipacoes_parcelas_pagador_id_idx" ON "antecipacoes_parcelas" USING btree ("pagador_id");--> statement-breakpoint
CREATE INDEX "antecipacoes_parcelas_categoria_id_idx" ON "antecipacoes_parcelas" USING btree ("categoria_id");--> statement-breakpoint
CREATE INDEX "anotacoes_user_id_idx" ON "anotacoes" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "passkey_user_id_idx" ON "passkey" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "compartilhamentos_pagador_shared_with_user_id_idx" ON "compartilhamentos_pagador" USING btree ("shared_with_user_id");--> statement-breakpoint
CREATE INDEX "compartilhamentos_pagador_created_by_user_id_idx" ON "compartilhamentos_pagador" USING btree ("created_by_user_id");--> statement-breakpoint
CREATE INDEX "session_user_id_idx" ON "session" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "lancamentos_conta_id_idx" ON "lancamentos" USING btree ("conta_id");--> statement-breakpoint
CREATE INDEX "lancamentos_categoria_id_idx" ON "lancamentos" USING btree ("categoria_id");--> statement-breakpoint
CREATE INDEX "lancamentos_antecipacao_id_idx" ON "lancamentos" USING btree ("antecipacao_id");

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,13 @@
"when": 1774891206703, "when": 1774891206703,
"tag": "0024_petite_lucky_pierre", "tag": "0024_petite_lucky_pierre",
"breakpoints": true "breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1776351838548,
"tag": "0025_burly_colonel_america",
"breakpoints": true
} }
] ]
} }

View File

@@ -32,57 +32,69 @@ export const user = pgTable("user", {
}).notNull(), }).notNull(),
}); });
export const account = pgTable("account", { export const account = pgTable(
id: text("id").primaryKey(), "account",
accountId: text("accountId").notNull(), {
providerId: text("providerId").notNull(), id: text("id").primaryKey(),
userId: text("userId") accountId: text("accountId").notNull(),
.notNull() providerId: text("providerId").notNull(),
.references(() => user.id, { onDelete: "cascade" }), userId: text("userId")
accessToken: text("accessToken"), .notNull()
refreshToken: text("refreshToken"), .references(() => user.id, { onDelete: "cascade" }),
idToken: text("idToken"), accessToken: text("accessToken"),
accessTokenExpiresAt: timestamp("accessTokenExpiresAt", { refreshToken: text("refreshToken"),
mode: "date", idToken: text("idToken"),
withTimezone: true, accessTokenExpiresAt: timestamp("accessTokenExpiresAt", {
mode: "date",
withTimezone: true,
}),
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt", {
mode: "date",
withTimezone: true,
}),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}).notNull(),
updatedAt: timestamp("updatedAt", {
mode: "date",
withTimezone: true,
}).notNull(),
},
(table) => ({
userIdIdx: index("account_user_id_idx").on(table.userId),
}), }),
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt", { );
mode: "date",
withTimezone: true,
}),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}).notNull(),
updatedAt: timestamp("updatedAt", {
mode: "date",
withTimezone: true,
}).notNull(),
});
export const session = pgTable("session", { export const session = pgTable(
id: text("id").primaryKey(), "session",
expiresAt: timestamp("expiresAt", { {
mode: "date", id: text("id").primaryKey(),
withTimezone: true, expiresAt: timestamp("expiresAt", {
}).notNull(), mode: "date",
token: text("token").notNull().unique(), withTimezone: true,
createdAt: timestamp("createdAt", { }).notNull(),
mode: "date", token: text("token").notNull().unique(),
withTimezone: true, createdAt: timestamp("createdAt", {
}).notNull(), mode: "date",
updatedAt: timestamp("updatedAt", { withTimezone: true,
mode: "date", }).notNull(),
withTimezone: true, updatedAt: timestamp("updatedAt", {
}).notNull(), mode: "date",
ipAddress: text("ipAddress"), withTimezone: true,
userAgent: text("userAgent"), }).notNull(),
userId: text("userId") ipAddress: text("ipAddress"),
.notNull() userAgent: text("userAgent"),
.references(() => user.id, { onDelete: "cascade" }), userId: text("userId")
}); .notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => ({
userIdIdx: index("session_user_id_idx").on(table.userId),
}),
);
export const verification = pgTable("verification", { export const verification = pgTable("verification", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
@@ -104,24 +116,30 @@ export const verification = pgTable("verification", {
// ===================== PASSKEY (WebAuthn) ===================== // ===================== PASSKEY (WebAuthn) =====================
export const passkey = pgTable("passkey", { export const passkey = pgTable(
id: text("id").primaryKey(), "passkey",
name: text("name"), {
publicKey: text("publicKey").notNull(), id: text("id").primaryKey(),
userId: text("userId") name: text("name"),
.notNull() publicKey: text("publicKey").notNull(),
.references(() => user.id, { onDelete: "cascade" }), userId: text("userId")
credentialID: text("credentialID").notNull(), .notNull()
counter: integer("counter").notNull(), .references(() => user.id, { onDelete: "cascade" }),
deviceType: text("deviceType").notNull(), credentialID: text("credentialID").notNull(),
backedUp: boolean("backedUp").notNull(), counter: integer("counter").notNull(),
transports: text("transports"), deviceType: text("deviceType").notNull(),
aaguid: text("aaguid"), backedUp: boolean("backedUp").notNull(),
createdAt: timestamp("createdAt", { transports: text("transports"),
mode: "date", aaguid: text("aaguid"),
withTimezone: true, createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}),
},
(table) => ({
userIdIdx: index("passkey_user_id_idx").on(table.userId),
}), }),
}); );
export const userPreferences = pgTable("preferencias_usuario", { export const userPreferences = pgTable("preferencias_usuario", {
id: uuid("id").primaryKey().default(sql`gen_random_uuid()`), id: uuid("id").primaryKey().default(sql`gen_random_uuid()`),
@@ -157,39 +175,30 @@ export const userPreferences = pgTable("preferencias_usuario", {
// ===================== PUBLIC TABLES ===================== // ===================== PUBLIC TABLES =====================
export const financialAccounts = pgTable( export const financialAccounts = pgTable("contas", {
"contas", id: uuid("id").primaryKey().default(sql`gen_random_uuid()`),
{ name: text("nome").notNull(),
id: uuid("id").primaryKey().default(sql`gen_random_uuid()`), accountType: text("tipo_conta").notNull(),
name: text("nome").notNull(), note: text("anotacao"),
accountType: text("tipo_conta").notNull(), status: text("status").notNull(),
note: text("anotacao"), logo: text("logo").notNull(),
status: text("status").notNull(), initialBalance: numeric("saldo_inicial", { precision: 12, scale: 2 })
logo: text("logo").notNull(), .notNull()
initialBalance: numeric("saldo_inicial", { precision: 12, scale: 2 }) .default("0"),
.notNull() excludeFromBalance: boolean("excluir_do_saldo").notNull().default(false),
.default("0"), excludeInitialBalanceFromIncome: boolean("excluir_saldo_inicial_receitas")
excludeFromBalance: boolean("excluir_do_saldo").notNull().default(false), .notNull()
excludeInitialBalanceFromIncome: boolean("excluir_saldo_inicial_receitas") .default(false),
.notNull() userId: text("user_id")
.default(false), .notNull()
userId: text("user_id") .references(() => user.id, { onDelete: "cascade" }),
.notNull() createdAt: timestamp("created_at", {
.references(() => user.id, { onDelete: "cascade" }), mode: "date",
createdAt: timestamp("created_at", { withTimezone: true,
mode: "date", })
withTimezone: true, .notNull()
}) .defaultNow(),
.notNull() });
.defaultNow(),
},
(table) => ({
userIdStatusIdx: index("contas_user_id_status_idx").on(
table.userId,
table.status,
),
}),
);
export const categories = pgTable( export const categories = pgTable(
"categorias", "categorias",
@@ -248,14 +257,6 @@ export const payers = pgTable(
uniqueShareCode: uniqueIndex("pagadores_share_code_key").on( uniqueShareCode: uniqueIndex("pagadores_share_code_key").on(
table.shareCode, table.shareCode,
), ),
userIdStatusIdx: index("pagadores_user_id_status_idx").on(
table.userId,
table.status,
),
userIdRoleIdx: index("pagadores_user_id_role_idx").on(
table.userId,
table.role,
),
}), }),
); );
@@ -285,6 +286,12 @@ export const payerShares = pgTable(
table.payerId, table.payerId,
table.sharedWithUserId, table.sharedWithUserId,
), ),
sharedWithUserIdIdx: index(
"compartilhamentos_pagador_shared_with_user_id_idx",
).on(table.sharedWithUserId),
createdByUserIdIdx: index(
"compartilhamentos_pagador_created_by_user_id_idx",
).on(table.createdByUserId),
}), }),
); );
@@ -317,10 +324,7 @@ export const cards = pgTable(
}), }),
}, },
(table) => ({ (table) => ({
userIdStatusIdx: index("cartoes_user_id_status_idx").on( accountIdIdx: index("cartoes_conta_id_idx").on(table.accountId),
table.userId,
table.status,
),
}), }),
); );
@@ -387,26 +391,33 @@ export const budgets = pgTable(
userIdCategoryIdPeriodUnique: uniqueIndex( userIdCategoryIdPeriodUnique: uniqueIndex(
"orcamentos_user_id_categoria_id_periodo_key", "orcamentos_user_id_categoria_id_periodo_key",
).on(table.userId, table.categoryId, table.period), ).on(table.userId, table.categoryId, table.period),
categoryIdIdx: index("orcamentos_categoria_id_idx").on(table.categoryId),
}), }),
); );
export const notes = pgTable("anotacoes", { export const notes = pgTable(
id: uuid("id").primaryKey().default(sql`gen_random_uuid()`), "anotacoes",
title: text("titulo"), {
description: text("descricao"), id: uuid("id").primaryKey().default(sql`gen_random_uuid()`),
type: text("tipo").notNull().default("nota"), // "nota" ou "tarefa" title: text("titulo"),
tasks: text("tasks"), // JSON stringificado com array de tarefas description: text("descricao"),
archived: boolean("arquivada").notNull().default(false), type: text("tipo").notNull().default("nota"), // "nota" ou "tarefa"
createdAt: timestamp("created_at", { tasks: text("tasks"), // JSON stringificado com array de tarefas
mode: "date", archived: boolean("arquivada").notNull().default(false),
withTimezone: true, createdAt: timestamp("created_at", {
}) mode: "date",
.notNull() withTimezone: true,
.defaultNow(), })
userId: text("user_id") .notNull()
.notNull() .defaultNow(),
.references(() => user.id, { onDelete: "cascade" }), userId: text("user_id")
}); .notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => ({
userIdIdx: index("anotacoes_user_id_idx").on(table.userId),
}),
);
export const savedInsights = pgTable( export const savedInsights = pgTable(
"insights_salvos", "insights_salvos",
@@ -460,7 +471,6 @@ export const apiTokens = pgTable(
.defaultNow(), .defaultNow(),
}, },
(table) => ({ (table) => ({
userIdIdx: index("tokens_api_user_id_idx").on(table.userId),
tokenHashIdx: uniqueIndex("tokens_api_token_hash_idx").on(table.tokenHash), tokenHashIdx: uniqueIndex("tokens_api_token_hash_idx").on(table.tokenHash),
}), }),
); );
@@ -524,6 +534,9 @@ export const inboxItems = pgTable(
table.userId, table.userId,
table.createdAt, table.createdAt,
), ),
transactionIdIdx: index("pre_lancamentos_lancamento_id_idx").on(
table.transactionId,
),
}), }),
); );
@@ -555,9 +568,6 @@ export const dashboardNotificationStates = pgTable(
userIdNotificationKeyUnique: uniqueIndex( userIdNotificationKeyUnique: uniqueIndex(
"dashboard_notification_states_user_id_key_unique", "dashboard_notification_states_user_id_key_unique",
).on(table.userId, table.notificationKey), ).on(table.userId, table.notificationKey),
userIdArchivedAtIdx: index(
"dashboard_notification_states_user_id_archived_idx",
).on(table.userId, table.archivedAt),
}), }),
); );
@@ -597,10 +607,14 @@ export const installmentAnticipations = pgTable(
.defaultNow(), .defaultNow(),
}, },
(table) => ({ (table) => ({
seriesIdIdx: index("antecipacoes_parcelas_series_id_idx").on(
table.seriesId,
),
userIdIdx: index("antecipacoes_parcelas_user_id_idx").on(table.userId), userIdIdx: index("antecipacoes_parcelas_user_id_idx").on(table.userId),
transactionIdIdx: index("antecipacoes_parcelas_lancamento_id_idx").on(
table.transactionId,
),
payerIdIdx: index("antecipacoes_parcelas_pagador_id_idx").on(table.payerId),
categoryIdIdx: index("antecipacoes_parcelas_categoria_id_idx").on(
table.categoryId,
),
}), }),
); );
@@ -700,6 +714,12 @@ export const transactions = pgTable(
table.cardId, table.cardId,
table.period, table.period,
), ),
// FK indexes: evitam seq scan em deletes/updates nas tabelas pai
accountIdIdx: index("lancamentos_conta_id_idx").on(table.accountId),
categoryIdIdx: index("lancamentos_categoria_id_idx").on(table.categoryId),
anticipationIdIdx: index("lancamentos_antecipacao_id_idx").on(
table.anticipationId,
),
// Dedup OFX: garante FITID único por usuário // Dedup OFX: garante FITID único por usuário
ofxFitIdUserIdIdx: uniqueIndex("lancamentos_ofx_fit_id_user_id_idx") ofxFitIdUserIdIdx: uniqueIndex("lancamentos_ofx_fit_id_user_id_idx")
.on(table.userId, table.ofxFitId) .on(table.userId, table.ofxFitId)
@@ -905,19 +925,25 @@ export const installmentAnticipationsRelations = relations(
// ===================== ATTACHMENTS ===================== // ===================== ATTACHMENTS =====================
export const attachments = pgTable("anexos", { export const attachments = pgTable(
id: uuid("id").primaryKey().default(sql`gen_random_uuid()`), "anexos",
userId: text("user_id") {
.notNull() id: uuid("id").primaryKey().default(sql`gen_random_uuid()`),
.references(() => user.id, { onDelete: "cascade" }), userId: text("user_id")
fileKey: text("chave_arquivo").notNull().unique(), .notNull()
fileName: text("nome_arquivo").notNull(), .references(() => user.id, { onDelete: "cascade" }),
fileSize: integer("tamanho_bytes").notNull(), fileKey: text("chave_arquivo").notNull().unique(),
mimeType: text("mime_type").notNull(), fileName: text("nome_arquivo").notNull(),
createdAt: timestamp("created_at", { mode: "date", withTimezone: true }) fileSize: integer("tamanho_bytes").notNull(),
.notNull() mimeType: text("mime_type").notNull(),
.defaultNow(), createdAt: timestamp("created_at", { mode: "date", withTimezone: true })
}); .notNull()
.defaultNow(),
},
(table) => ({
userIdIdx: index("anexos_user_id_idx").on(table.userId),
}),
);
export const transactionAttachments = pgTable( export const transactionAttachments = pgTable(
"lancamento_anexos", "lancamento_anexos",
@@ -953,6 +979,9 @@ export const importCategoryMappings = pgTable(
}, },
(table) => ({ (table) => ({
pk: primaryKey({ columns: [table.userId, table.descriptionKey] }), pk: primaryKey({ columns: [table.userId, table.descriptionKey] }),
categoryIdIdx: index("import_category_mappings_category_id_idx").on(
table.categoryId,
),
}), }),
); );