perf(排名): 打开排名页快一些,看「全服 · 全部」从 0.27 秒降到 0.08 秒
Deploy / deploy (push) Canceled after 0s

做对时刻在库里换成毫秒、按人聚成数组再取回,不再一行一个 timestamptz
逐条解析(全服全部历史六万多行,光解析就占 150ms)。名次与原来完全一致。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-03 10:32:32 +08:00
1 parent f06bb063b4
commit 41d2e46aa9
2 files changed
+32 -37

No files matched your search

+1 -8
View File
@@ -252,14 +252,7 @@ rankingRoutes.get('/rankings/champions', optionalAuth, async (c) => {
const champions: WeeklyChampion[] = []
for (let k = 1; k <= CHAMPION_WEEKS; k++) {
const from = thisWeek - k * WEEK_MS
const inWeek = entrants.map((entrant) => {
const keep = entrant.times.map((time, index) => [time, entrant.stamps[index]!] as const).filter(([time]) => time >= from && time < from + WEEK_MS)
return {
...entrant,
times: keep.map(([time]) => time),
stamps: keep.map(([, stamp]) => stamp),
}
})
const inWeek = entrants.map((entrant) => ({ ...entrant, times: entrant.times.filter((time) => time >= from && time < from + WEEK_MS) }))
const [top] = standings(inWeek, null, false)
if (!top) continue
champions.push({
+31 -29
View File
@@ -36,9 +36,8 @@ export interface Entrant {
className: string | null
avatar: string | null
mood: string | null
/** 每道题第一次做对的时刻,升序;毫秒数用来比较,原文留着给出参 */
/** 每道题第一次做对的时刻(毫秒),升序 */
times: number[]
stamps: string[]
}
export interface Standing {
@@ -64,10 +63,26 @@ export function avatarOf(avatar: string | null) {
* 先按 (人, 题) 取全部历史里的最早一次,再用 having 卡 `>= since` —— 这样「这学期」数的是
* 这学期**第一次**做对的题,以前做对过的老题这学期重交一遍不算。比赛里的提交不算。
* 走 `submission_public_metrics_idx`(user_id, problem_id, result, create_time,WHERE
* contest_id IS NULL),全服全部历史实测 24ms。
* contest_id IS NULL)。
*
* 时刻在库里就换成毫秒数、按人排好聚成数组再拿回来:全服全部历史有六万多个 (人, 题),
* 一行一个 timestamptz 的话,光 db/index.ts 那个保留微秒的 parser 加 `Date.parse` 就
* 要 150ms(两核上),库里的查询本身才 40ms。截到毫秒和原来 `Date.parse` 比较的精度一样,
* 名次不变;`reachedAt` 只拿来显示「比你早多久」,用不着微秒。
*/
export async function loadEntrants(where: SQL, since: string | null): Promise<Entrant[]> {
const firstSolved = min(schema.submission.createTime)
const firsts = db
.select({
userId: schema.submission.userId,
at: sql<number>`floor(extract(epoch from ${firstSolved}) * 1000)::float8`.as('at'),
})
.from(schema.submission)
.innerJoin(schema.user, eq(schema.user.id, schema.submission.userId))
.where(and(where, isNull(schema.submission.contestId), inArray(schema.submission.result, SOLVED_RESULTS)))
.groupBy(schema.submission.userId, schema.submission.problemId)
.having(since ? gte(firstSolved, since) : undefined)
.as('firsts')
const [people, solves] = await Promise.all([
db
.select({
@@ -81,33 +96,20 @@ export async function loadEntrants(where: SQL, since: string | null): Promise<En
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
.where(where),
db
.select({ userId: schema.submission.userId, at: firstSolved })
.from(schema.submission)
.innerJoin(schema.user, eq(schema.user.id, schema.submission.userId))
.where(and(where, isNull(schema.submission.contestId), inArray(schema.submission.result, SOLVED_RESULTS)))
.groupBy(schema.submission.userId, schema.submission.problemId)
.having(since ? gte(firstSolved, since) : undefined),
.select({ userId: firsts.userId, times: sql<number[]>`array_agg(${firsts.at} order by ${firsts.at})` })
.from(firsts)
.groupBy(firsts.userId),
])
const byUser = new Map<number, string[]>()
for (const row of solves) {
if (!row.at) continue
const list = byUser.get(row.userId)
if (list) list.push(row.at)
else byUser.set(row.userId, [row.at])
}
return people.map((person) => {
const pairs = (byUser.get(person.id) ?? []).map((stamp) => [Date.parse(stamp), stamp] as const).sort((a, b) => a[0] - b[0])
return {
id: person.id,
username: person.username,
className: person.className,
avatar: avatarOf(person.avatar),
mood: person.mood?.trim() || null,
times: pairs.map(([time]) => time),
stamps: pairs.map(([, stamp]) => stamp),
}
})
const byUser = new Map(solves.map((row) => [row.userId, row.times]))
return people.map((person) => ({
id: person.id,
username: person.username,
className: person.className,
avatar: avatarOf(person.avatar),
mood: person.mood?.trim() || null,
times: byUser.get(person.id) ?? [],
}))
}
/**
@@ -127,7 +129,7 @@ export function standings(entrants: Entrant[], cutoff: number | null, includeZer
entrant,
solved,
reached: solved ? entrant.times[solved - 1]! : Number.POSITIVE_INFINITY,
reachedAt: solved ? entrant.stamps[solved - 1]! : null,
reachedAt: solved ? new Date(entrant.times[solved - 1]!).toISOString() : null,
}
})
.filter((row) => includeZero || row.solved > 0)