perf(提交列表): 按用户名、题号筛选走索引,不再扫全表
Some checks failed
Deploy / deploy (push) Has been cancelled

用户名筛选:user_id 先查成字面列表,不再把子查询夹在 OR 里(那样整条 OR 不可索引),
加 trigram 索引接住 ilike '%x%';翻页先圈出匹配行再排序取页,避开规划器顺着时间索引
倒扫、边扫边滤的计划。快照上查一个班 70~107ms → 5~14ms;匹配 9.5 万条的年级前缀
从 34ms 变成 70~90ms,实际不这么查。

题号筛选:先解析成 problem.id,加 (problem_id, create_time, id) 部分索引。老题和
不存在的题号不再倒扫大半张表(34~67ms → 4ms),题号筛选也能走游标深翻页,
count 不再 join problem。

迁移 0015 装 pg_trgm(官方镜像自带 contrib,trusted 扩展)。39 个筛选组合的响应
与改动前逐字节一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xg91q3JsunDE7EYoi9G3i2
This commit is contained in:
2026-09-13 23:47:24 -06:00
parent fe4fc46243
commit 6e63866cc9
5 changed files with 3850 additions and 39 deletions

View File

@@ -545,6 +545,23 @@ export const submission = pgTable("submission", {
*/
index("submission_language_time_idx").using("btree", table.language.asc().nullsLast(), table.createTime.asc().nullsLast()).where(sql`${table.contestId} is null`),
index("submission_result_time_idx").using("btree", table.result.asc().nullsLast(), table.createTime.asc().nullsLast()).where(sql`${table.contestId} is null`),
/**
* 提交列表的「题号」筛选。路由先把题号解析成 problem.id见 routes/submission.ts 的
* problemFilter这条索引才用得上等值定位到一道题剩下两列正好是翻页的全序
* 深翻页的游标也照走。列方向同上面几条,全 ASC 靠 Backward 扫。
*
* problem_user_idx 以 problem_id 打头,但不带时间:一道题最近几个月没人交的话,
* 规划器照样选分页索引倒扫、边扫边滤。快照实测 10476 月之后没人交17ms 倒扫
* 2 万行、3017最后一次在 2024 年59ms 倒扫 8.2 万行、不存在的题号扫完全表。
*/
index("submission_public_problem_time_idx").using("btree", table.problemId.asc().nullsLast(), table.createTime.asc().nullsLast(), table.id.asc().nullsLast()).where(sql`${table.contestId} is null`),
/**
* 提交列表的「用户名」筛选是 `ilike '%x%'`btree 帮不上,只有 trigram 能索引中缀匹配。
* 扩展在迁移 0015 里装(官方 postgres 镜像自带 contribpg_trgm 是 trusted 扩展)。
* 3MB。模式不足 3 个字符时 trigram 抽不出东西,照旧全表扫——那种前缀匹配大半张表,
* 扫表本来就是对的计划。
*/
index("submission_public_username_trgm_idx").using("gin", table.username.op("gin_trgm_ops")).where(sql`${table.contestId} is null`),
/**
* 覆盖索引,专门给「在全部公开提交上做聚合」那几个接口用:教师统计不填班级、
* 活跃榜、题目 AC 趋势。它们慢的**不是聚合本身,是为了读这四个小列把 145MB 的堆