Files
OJ2/apps/api/src/db
yuetsh 6c2381ad2c
Some checks failed
Deploy / deploy (push) Has been cancelled
perf(提交列表): 越早的页越慢,最后一页要等 1.2 秒
翻页用的是 LIMIT n OFFSET m,而 Postgres 对 OFFSET 没有捷径:前 m 行必须真的
产出再丢掉,丢弃又发生在 join 之后,每一行都白回了一次表。生产快照(10.4 万条
公开提交)实测最后一页 1258ms、碰了 95347 个 buffer。最早那几页平时没人翻,
数据页从来不在缓存里,全是冷读,所以感受上比新的几页慢得多。

改成两步:先只 select create_time / id 数到第 m 行拿游标——这两列正好是部分索引
的全部内容,跳过 m 行走 Index Only Scan,Heap Fetches 为 0,纯在索引页里数数;
再拿这一行做 keyset 回查,只回表取 limit 行。同一页降到约 9ms、885 个 buffer,
端到端 HTTP 10.7ms。代价变成 O(m) 个索引条目而不是堆页,按快照密度外推,
涨到 100 万条时最深一页仍在几十毫秒量级。

接口签名和前端都没动,页码跳转照旧。offset 为 0、以及按题号筛选时(条件在
problem 表上,第一步得跟着 join,index-only 就没了)退回普通 offset。

部分索引从 (create_time) 换成 (create_time, id):create_time 由
new Date().toISOString() 生成,只有毫秒精度,不是全序,游标用 <= 回查时同毫秒的
上一页末行会重复出现在下一页页首。加 id 之后两步走同一个顺序。索引 2.3MB → 6.9MB。

索引两列都建成默认 ASC,靠 Index Only Scan Backward 反着扫。别照着 ORDER BY
写成 (create_time DESC, id DESC):ORDER BY 的 DESC 默认 NULLS FIRST,索引的 DESC
默认 NULLS LAST,规划器认为出不了序,会退化成 external merge sort(5.2MB 落盘),
比不加索引还糟。这一条已写进 schema.ts 和迁移文件的注释。

正确性:在快照上把新旧写法返回的 id 序列逐页比对,14 个 offset × 4 个 limit
共 56 组全部一致,含末尾残页与越界。

比赛提交列表暂不改:单场比赛撑死几千条,offset 不构成问题。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:13:38 -06:00
..