perf(worker): 「我的订单」列表改成先取 id 再取详情,排序阶段不再物化宽列

打手端我的订单的排序是表达式:
  COALESCE(CASE WHEN wo.assigned_worker_id = $1 THEN wo.assigned_at END,
           CASE WHEN worker_share.status != 'cancelled' THEN worker_share.created_at END)
  DESC NULLS LAST, wo.id DESC
它依赖打手参数与拼单表,**没有任何索引能支撑**,规划器只能把该打手全部可见单
连同宽列(每行约 1.2KB material_json)物化后排序。生产实测(真实 service 路径):
名下 6609 单的打手首访 1112ms、5198 单的 808ms、3140 单的 772ms,每次读约 60MB
(列表缓存 2 秒一失效,所以这个代价每天要重复付很多次)。

改法:该排序走"延迟分页"——第一阶段只 SELECT wo.id(窄投影 + 同一 ORDER BY +
同一 LIMIT/OFFSET),第二阶段用 `WHERE wo.id = ANY($1::bigint[])` 取这一页的详情,
再按 id 顺序还原(ANY 不保证顺序)。行集、顺序、总数语义完全不变。

验证:后端 542 项单测全绿(新增 1 项守卫);生产上先用部署前的 id 顺序留底,
部署后用同一批打手比对首 20 条 id 与总数,并对比首访耗时。
This commit is contained in:
yml2213
2026-10-01 15:29:49 +08:00
parent 987553909d
commit 21cd9135a2
2 changed files with 52 additions and 0 deletions
@@ -2787,3 +2787,24 @@ test('统计的「状态+日期范围」组合必须有 created_at 键的覆盖
'迁移在事务内执行,不能使用 CONCURRENTLY',
)
})
test('打手端「我的订单」必须走"先取 id 再取详情"的延迟分页', async () => {
const { readFile } = await import('node:fs/promises')
const src = await readFile(
new URL('../../repositories/worker-platform/work-order-query-repo.ts', import.meta.url),
'utf8',
)
assert.ok(
src.includes("sort === 'worker_claimed_at_desc' && workerSharingId && !pinnedFirst"),
'缺少打手排序的延迟分页分支:该排序是表达式,无索引可支撑,会物化全部可见单的宽列',
)
assert.ok(
src.includes('SELECT wo.id'),
'第一阶段必须只取 id(窄投影),否则排序仍会读 material_json',
)
assert.ok(
src.includes('WHERE wo.id = ANY($1::bigint[])'),
'第二阶段必须按 id 批量取详情,而不是重新跑一遍带排序的宽查询',
)
assert.ok(src.includes('rowsById.get(id)'), 'ANY() 不保证顺序,必须按 id 顺序还原分页结果')
})