perf(数据库): user 表补两个索引,班级过滤和活跃人数统计不再全表扫
`user` 原来只有主键和 username 唯一约束两个索引 —— 它是 drizzle-kit pull 从 Django 建的表拉出来的,Django 那边也没建过别的。于是两条常走的查询都是全表扫: - `problems/:id/beat-count` 里「近两年登录过的活跃人数」按 is_disabled + last_login 过滤,每打开一次题目详情算一遍; - `routes/classroom.ts` 的 loadClassUsers 按 class_name(或年级前缀 like '241%') 取学生,班级榜、班级对比、AI 学情的排名 scope 都走它。 两千行的表现在扫起来确实不贵,但这是随人数线性涨的那类成本,而且比在应用层加缓存 更根本:不引入陈旧,也不需要考虑失效。 `user_active_idx` 的列序是 (is_disabled, last_login):等值条件在前、范围条件在后。 迁移只有两条 CREATE INDEX,无附带改动;本机 db:migrate 跑过,两个索引都在。 索引效果没法在本机验 —— dev 库只有 12 条提交、几十个用户,规划器无论如何都走 seq scan,要到生产(12 万提交)才看得出来。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
This commit is contained in:
2
apps/api/src/db/0011_user_lookup_indexes.sql
Normal file
2
apps/api/src/db/0011_user_lookup_indexes.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE INDEX "user_active_idx" ON "user" USING btree ("is_disabled","last_login" DESC NULLS FIRST);--> statement-breakpoint
|
||||||
|
CREATE INDEX "user_class_name_idx" ON "user" USING btree ("class_name");
|
||||||
3936
apps/api/src/db/meta/0011_snapshot.json
Normal file
3936
apps/api/src/db/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,13 @@
|
|||||||
"when": 1788409961010,
|
"when": 1788409961010,
|
||||||
"tag": "0010_fk_cascade_on_delete",
|
"tag": "0010_fk_cascade_on_delete",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1788788493497,
|
||||||
|
"tag": "0011_user_lookup_indexes",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -664,6 +664,12 @@ export const user = pgTable("user", {
|
|||||||
className: text("class_name"),
|
className: text("class_name"),
|
||||||
}, (table) => [
|
}, (table) => [
|
||||||
unique("user_username_key").on(table.username),
|
unique("user_username_key").on(table.username),
|
||||||
|
// 「近两年登录过的活跃人数」—— problems/:id/beat-count 每次打开题目详情都要算一遍,
|
||||||
|
// 而这张表原来只有主键和 username 两个索引,那句统计是全表扫。
|
||||||
|
index("user_active_idx").using("btree", table.isDisabled.asc().nullsLast(), table.lastLogin.desc().nullsFirst()),
|
||||||
|
// 按班级 / 按年级(class_name like '241%')取学生:班级榜、班级对比、AI 学情的
|
||||||
|
// 排名 scope 都走它,见 routes/classroom.ts 的 loadClassUsers。
|
||||||
|
index("user_class_name_idx").using("btree", table.className.asc().nullsLast()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const problemsetBadge = pgTable("problemset_badge", {
|
export const problemsetBadge = pgTable("problemset_badge", {
|
||||||
|
|||||||
Reference in New Issue
Block a user