Some checks failed
Deploy / deploy (push) Has been cancelled
翻页用的是 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>