Files
OJ2/apps/api
yuetsh 80f3b21e95 perf(索引): 0012 删 21 个冗余索引,0013 加 4 个筛选/聚合索引
0012 —— 删的都是 Django 建的,列是某个复合索引的最左前缀,规划器本来就走那一个,
多出来的只是每次写入多维护一棵树:17 个前缀被覆盖的、3 个 _like(text_pattern_ops)
副本、1 个同表同列的完全重复(problemset_submission.user_id 上有两个)。
索引 107 → 86 个,32MB → 29MB。

删之前逐条确认过复合索引的第一列就是被删索引的那一列,删之后 19 条代表性查询的
执行计划逐条对过,没有一条退化成 Seq Scan,只是换了覆盖它的那个索引;级联删除会
用到的外键检查路径也都还有索引可走。

0013 —— 拿 auto_explain 把 60 多个读接口打一遍抓出来的真实慢查询,候选索引一个个
建出来实测:

- submission (language, create_time) WHERE contest_id IS NULL — 3.2MB
  语言筛选原来一个索引都没有,count 固定 75~82ms / 18448 buffers,筛什么值都一样。
  改后 Python3(占 8 成)80 → 11ms、C 77 → 1.6ms、SQL 82 → 0.06ms。更要命的是冷门
  语言翻页:Python2 只有 3 条全是 2022 年的,分页索引得从最新倒扫到底,43ms 全表扫
  → 0.02ms
- submission (result, create_time) WHERE contest_id IS NULL — 3.2MB
  count result=-1 75 → 2.0ms、result=-2 36 → 1.0ms
- submission (user_id, problem_id, result, create_time) WHERE contest_id IS NULL — 4.2MB
  覆盖索引,给「在全部公开提交上做聚合」那几个接口。它们慢的不是聚合本身,是为了读
  这四个小列把 145MB 的堆翻一遍(code 和 info 占了这张表绝大部分体积,聚合一列都用
  不上)。走 Index Only Scan 只读 6MB:教师统计全站 186 → 49ms(消掉 4.2MB 落盘
  排序)、活跃榜 108 → 18ms、AC 趋势 120 → 41ms
- flowchart_submission (create_time) — 64kB
  列表分页从 hash join 全表再 top-N 排序(4.5ms / 551 buffers)变成 0.19ms / 47

全列 ASC NULLS LAST 靠反向扫,理由同 submission_public_create_time_id_idx 那段注释。
动手前把三种写法在库上对了一遍:两列 ASC 和 create_time DESC NULLS FIRST 一样快,
写成 DESC NULLS LAST 规划器直接不认这条索引、回落到分页索引带 Filter,注释没说错。

回归检查:不带筛选的列表和深翻页仍然走 submission_public_create_time_id_idx,没被
新索引抢走。submission_result_37e2f67a 留着并补了注释 —— 它看着像被新的部分索引
覆盖了,但那条带 WHERE contest_id IS NULL,管不了全库含比赛按 result 统计(实测删掉
之后 count(*) where result in (6,7) 从走索引掉回 75ms 全表扫)。

两条迁移都在生产结构副本和本机 dev 库各跑过一遍。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqjE6qPo67fqVDKn6Bx7yd
2026-09-08 18:29:53 -06:00
..