流程图提交量涨上去之后,先扛不住的是教师统计面板:它一条不带 limit 的 select
把整个时间窗的行拉进内存再用 JS 算,词云那个 3000 条上限是在 JS 里截的,行早就
全回来了。列表那边则是 `select({ flowchart: 整行, problem: 整行 })`,把
mermaid_code、flowchart_data、三个 AI 文本列和整张题目表一起拉回来,响应一个
都用不到(快照实测流程图行均 4.9KB、题目行均 2.2KB,10 行一页白拉 ~70KB,
limit=250 时 1.7MB)。
- 列表改成白名单列 flowchartListColumns,对齐 submission 那边的
submissionListColumns(那边同样刻意不取 code / info)。
- 题号 / 用户名筛选先解析成 flowchart_submission 自己的列,count 因此一个 join
都不用挂,回得到最小索引上的 index-only scan;筛条件落在驱动表上,规划器也走
得上 flowchart_user_time_idx / flowchart_problem_time_idx。
- 统计面板拆成五条各自和行数脱钩的查询:数值聚合、等级分布、各项平均分
(jsonb_each + group by)、词云原料(order by ... limit 3000)、谁没做。
每项满分改从词云那批行里顺手取,省掉一次 21 万行的排序。
- matchedUsers() 从 submission.ts 挪进 helpers.ts,两条统计共用。
拿生产快照(2134 条)复制一份、另插三行脏数据(标量 jsonb、数组 jsonb、分数和
满分写成字符串),新旧两版各跑 24 个请求组合:23 个逐字节一致;剩下 1 个只是三行
create_time 完全相同的记录先后不同 —— 既有的不确定性(ORDER BY create_time 不是
全序),留给后面的 keyset 分页一并解决。
把表灌到 5.3 万 / 21.3 万行实测(HTTP 端到端,5 次取最好,旧 → 新):
列表 limit=10 35 → 4 ms | 56 → 8 ms
列表 limit=250 36 → 5 ms | 54 → 8 ms
列表 offset=5万 108 → 39 ms | 57 → 45 ms
列表 按班级 33 → 4 ms | 53 → 8 ms
统计 全部时段 288 → 187 ms | 1169 → 704 ms
统计 一个班 143 → 43 ms | 388 → 129 ms
统计 一道题 94 → 194 ms | 339 → 277 ms
「统计 一道题」在 5 万行量级是退步的:那个筛选命中全表 23% 的行,PG 侧的 jsonb
算子比「原样输出让 Bun 去 parse」更费 CPU,要到 21 万行才反超。它跟的是筛出来的
行数、不跟总量走,200ms 的教师面板可以接受,没有再调。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ADMIN_ROLES, TEACHER_ROLES, type SampleUser } from "@oj2/contract"
|
||||
|
||||
import { and, count, eq, notInArray } from "drizzle-orm"
|
||||
import { and, count, eq, ilike, notInArray } from "drizzle-orm"
|
||||
|
||||
import type { AuthUser } from "../auth/session"
|
||||
import { db, schema } from "../db"
|
||||
@@ -128,3 +128,33 @@ export async function countFailedSubmissions(userId: number, problemId: number)
|
||||
)
|
||||
return failed?.value ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名模糊匹配到的账号。统计的两件事都从它出发:**筛哪些提交**(拿 id),
|
||||
* 以及**花名册**(班级人数、谁没做,见调用处的过滤)。
|
||||
*
|
||||
* 这里必须查 `user` 表而不是 `submission.username` —— 后者是提交那一刻冻结的
|
||||
* 快照,学生改名之后旧提交还挂着旧名字,`ilike submission.username` 匹配不上。
|
||||
*
|
||||
* 生产快照实测(2026-09-08):24 级数媒两个班改成编号制用户名之后,85 人的
|
||||
* 提交挂在旧名下。查 `ks249` 旧口径 0 条 / 新口径 7 条 —— 整个班 48 人全掉进
|
||||
* 「一条没交」;查 `ks248` 20 条 / 54 条,13 个人的成绩查不出来。
|
||||
*
|
||||
* 返回**全部**匹配到的账号,禁用的和教师也在内 —— 「谁交过」不该受这两个条件
|
||||
* 影响。花名册那一份在调用处再筛(未禁用 + 普通用户),教师和管理员不进分母。
|
||||
*
|
||||
* 代码提交和流程图两条统计都走这里。流程图那张表连冻结用户名都没有(只有
|
||||
* `user_id`),更是只能从这儿拿 id。
|
||||
*/
|
||||
export async function matchedUsers(username: string) {
|
||||
return db
|
||||
.select({
|
||||
id: schema.user.id,
|
||||
username: schema.user.username,
|
||||
className: schema.user.className,
|
||||
isDisabled: schema.user.isDisabled,
|
||||
adminType: schema.user.adminType,
|
||||
})
|
||||
.from(schema.user)
|
||||
.where(ilike(schema.user.username, `%${username}%`))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user