种子数据保真度策略

Seed Data Fidelity Strategy — 基于 DB 依赖关系图分析的讨论结论 — 2026-02-08

1. 问题起源

在构建 DB 数据流依赖关系图 的过程中,我们发现当前的 perf-seed 种子数据生成脚本存在一个根本性问题:种子数据的生成路径和生产环境的数据流路径不一致。

当前 seed 使用 BatchInserter 通过 bulk INSERT + ON CONFLICT DO NOTHING 直接向数据库灌数据,完全绕过了:

2. 依赖关系图的四种边类型与健康度

DB 依赖关系图 中,我们将表间关系分为四种类型。这四种类型有明确的健康度排序:

边类型 执行层级 强制性 健康度 说明
FK Constraint 数据库层 无法绕过 最健康 任何方式写 SQL 都会被强制检查,是最可靠的数据完整性保证
DB Trigger 数据库层 自动触发 健康 数据库层自动执行,应用代码无法绕过,但有隐蔽性(调试困难)
Business Logic 应用层 仅限 service 调用 脆弱 只在 JavaScript service 代码里保证多表一致性。直接 SQL、其他脚本、seed 都可以绕过
Seed Derivation 脚本层 仅限 seed 不适用 仅反映测试数据如何生成,与生产运行无关
核心洞察

FK 和 Trigger 是数据库层强制执行的,属于"健康"关系。Business Logic 是应用层约定的,属于"脆弱"关系 — 有 Green 边但没有 Blue 边兜底的表关系是最大风险点。Seed Derivation 的理想状态是消失 — 即 seed 不应该有自己独立的数据流路径,而应该复用生产的 Business Logic 路径。

3. 核心结论:种子数据应模拟生产数据流

决策

种子数据的生成路径应该和生产环境的数据流保持一致。seed 脚本应复用 service 层的核心逻辑函数,而不是用 bulk INSERT 绕过所有校验。

为什么不能"长得像就行"

即使只测读性能,绕过业务逻辑生成的数据也会导致测试结论不可靠:

4. 实施策略:分层保真度

不是所有表都需要同等保真度。按表的角色分三层:

层级 适用表 生成方式 原因
高保真 appointments, sub_appointments, transactions, invoices, receipts, refunds, checkins 复用 service 层函数 核心业务表,数据量最大,且是性能瓶颈集中区。写入顺序、约束校验、触发器触发都必须走完整路径
中保真 employees, employee_work_schedules_daily, guests, customer_memberships, day_end_closeouts 模拟 service 层写入顺序,确保 FK 和触发器正常工作 这些表被核心表依赖。必须确保数据完整性(如 daily schedules 必须触发 available_times 生成)
低保真 guest_tags, promotion_send_logs, application_logs, auth_audit_logs, points_transactions bulk INSERT 可接受 末端数据,不触发下游逻辑。只需要行数和分布合理即可

高保真层的具体做法

当前 seed(绕过一切)
BatchInserter.add(row) bulk INSERT ... ON CONFLICT DO NOTHING

目标 seed(复用业务逻辑)
service.createAppointment(data) appointments + sub_appointments + available_times 触发

关键:不需要调 HTTP API。直接在 seed 脚本中 require service 模块,跳过认证/权限中间件,但保留:

性能平衡

复用 service 层会比 bulk INSERT 慢。但可以通过以下方式缓解:

5. 当前 seed 已知的保真度缺陷

问题 影响 涉及的层
L4 直接 INSERT appointments,跳过时间冲突检测 同一技师同一时段可能有多个预约,数据分布不真实 L4
L2 写入 employee_work_schedules 而非 employee_work_schedules_daily 触发器 fn_generate_available_times 未触发,employee_available_times 可能为空 L2
ON CONFLICT DO NOTHING 吞掉 FK 违规 实际插入行数可能少于 expected,表基数偏小(如 .checkpoint.json 中 appointments inserted 11,110,000 > expected 10,950,000) 全部
L5 transactions 没走支付确认流程 缺少 transaction_history 的完整状态变更链(pending→confirmed→completed) L5
L8 guest_tags 直接 INSERT,没走标签评估引擎 标签分布可能与真实评估结果不一致 L8

6. 预约数据保留策略

决策:不删除,按月分区(对标 Zenoti / Boulevard)

预约数据永久保留,不做时间窗口删除。通过 PostgreSQL 表分区(按月)实现查询性能隔离 — 查询只扫描需要的分区,历史数据不影响热数据性能。

性能测试 seed 保持 2 年(730 天)数据量。目的不是减少数据让查询变快,而是用真实数据量暴露需要加索引、需要分区、需要归档的地方。

employee_available_times 只需生成未来 4 周(28 天),因为预约最多提前 4 周。历史预约不需要 available_times 支撑。

7. 优化路线图

  1. Phase 1 - 标记风险(已完成):在 DB 依赖关系图 中可视化四种数据流类型,识别 "Green without FK backup" 的脆弱关系
  2. Phase 2 - 触发器链修复:让 L2 写入 employee_work_schedules_daily 而非 employee_work_schedules,确保 employee_available_times 被正确生成
  3. Phase 3 - 核心表高保真:将 L4(appointments)和 L5(transactions)改为调用 service 层函数,加入时间冲突检测和支付状态流转
  4. Phase 4 - 性能优化:为高保真模式实现并发 worker + 批次事务,确保 250M 行数据在合理时间内完成

7. 相关文档