⚠️ 自动镜像 · 此页由
docs-site/scripts/mirror-adr.mjs从docs/adr/0009-task-events-table-and-partition.md生成,请勿直接编辑此处;改源文件后pnpm docs:build会自动同步。
ADR-0009: task_events 表与按月分区方案
- Status: Proposed
- Date: 2026-05-06
- Supersedes: —
- Related: ADR-0006 (predictions 月分区), ADR-0008 (audit_logs 不可变 trigger)
Context
v0.8.4 落「效率看板 / 人员绩效」epic 时新增了高频写表 task_events:
- 每位标注员 task 切换时 push 一条
{task_id, user_id, project_id, kind, started_at, ended_at, duration_ms, ...} - 写入路径:工作台
useSessionStats.ts缓冲 →POST /auth/me/task-events:batch→ Celerypersist_task_events_batch→ INSERT - 读取路径:物化视图
mv_user_perf_daily每小时聚合 + AdminPeoplePage 详情页直接读 耗时直方图原始数据
写入量级(产品线规模假设):
- 单标注员每分钟 ~1-2 条(task 切换粒度)
- 1 万标注员 / 8 小时工作 / 60 min ≈ 480 万行/日,月 ≈ 1.4 亿行
- 当前内部部署量级 ≤ 50 标注员 / 8h ≈ 2.4 万行/日 ≈ 月 70 万
本期(v0.8.4)选择不分区,避免引入复合主键 + 多分区维护负担。但 schema 设计要为未来按月 RANGE 分区留好出口。
Decision
Stage 1(v0.8.4,本期):
- 普通表
task_events,PK =id(UUID) - 索引:
(user_id, started_at DESC)/(project_id, started_at DESC)/(kind, started_at DESC)/(task_id) - 物化视图
mv_user_perf_daily聚合源 - Celery beat hourly REFRESH MATERIALIZED VIEW CONCURRENTLY
Stage 2(触发条件满足后另起 PR):
- 单月 INSERT > 100k 或总行数 > 1M
- 迁移 schema 为
PARTITION BY RANGE(started_at),PK 改为(id, started_at) - 子分区命名:
task_events_y{YYYY}m{MM}(与audit_logs一致) - 创建 12 个月历史分区 + 3 个月未来分区
- Celery beat 每月 25 日
ensure_future_task_events_partitions(参考 ADR-0008) - 冷数据归档:超过
task_events_retention_months(默认 24)的分区导出到 MinIO 后 DROP - 物化视图:分区表上 REFRESH 不变(不依赖底层是否分区)
Consequences
正向:
- v0.8.4 实现路径短、迁移代价小,能在不阻塞前端落地的窗口内完成。
- Schema 列与索引与未来分区表对齐,Stage 2 只需改表结构 + 数据迁移,应用代码无需改动。
负向 / 风险:
- 单表行数若超出预期增长(例如客户突击导入老数据集),查询性能会先于触发阈值劣化; 需在监控里盯
pg_stat_user_tables.n_live_tupfortask_events。 - 工作台批量写在 broker 抖动时 fallback 到同步路径(在
me.py中 inline INSERT), 此时单批次最多 200 行 INSERT 阻塞 1 个请求;建议 staging 环境压测 P95 < 200ms。 - 物化视图
mv_user_perf_daily当日 lag ≤ 1 小时;端点优先读视图, 当日窗口需用「视图 ∪ 直查 task_events」联合(参考 dashboard.py 的实现)。 - 分布式部署时,
pg_get_serial_sequence对 UUID 不适用 — 本表用 UUID 主键避开了audit_logs的 sequence 同步问题。
Stage 2 触发流程(草案)
- 监控触发:
SELECT count(*) FROM task_events> 1,000,000 或pg_stat_get_inserted单月 > 100k。 - 冻结 Celery 写入(
task_events_async = False,sync fallback 持续)。 - 在维护窗口运行迁移:
ALTER TABLE task_events RENAME TO task_events_legacy- 创建分区父表
task_eventsPARTITION BY RANGE (started_at),PK=(id, started_at) - 创建覆盖 [min(legacy.started_at) 月, current+3] 的子分区
INSERT INTO task_events SELECT * FROM task_events_legacy- 重建 mv_user_perf_daily 唯一索引(其分区表已天然支持 CONCURRENTLY refresh)
- DROP TABLE task_events_legacy
- 恢复
task_events_async = True。
预计 1M 行迁移在 2c8g 实例上 < 5 min。
Open Questions
- 是否给标注会话粒度(
session_id)单独建二级索引?目前所有查询都按user_id + started_at走, 无人统计「单会话内 task 切换分布」,暂不加。 - mv_user_perf_daily 是否需要按
kind进一步分裂为两个视图?目前查询都带WHERE kind=…filter, 视图本身按(user_id, project_id, kind, day)UNIQUE 索引已能命中,不分裂。