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:
2026-09-07 07:42:46 -06:00
parent ec1509c46d
commit 57bc652629
4 changed files with 3951 additions and 0 deletions

View 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");

File diff suppressed because it is too large Load Diff

View File

@@ -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
} }
] ]
} }

View File

@@ -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", {