用嵌套数组存答案时每个对象须含question_id和统一value类型,题干单独存于questions集合;禁用q1/q2等固定字段名,因其导致结构僵化、跳题逻辑错乱;answers应独立成集合以支持高效聚合查询与原子更新。

直接结论:用嵌套数组存答案,每个对象带 question_id 和统一 value 类型,别用固定字段名或序号;题干单独建 questions 集合,靠 question_id 关联。
为什么不能用 q1、q2 这类字段名?
新增题目就得改后端代码和所有历史文档结构,MongoDB 不支持 ALTER TABLE。更糟的是,跳题逻辑一变、题序一调,q3 就可能对应错题干。前端改个题号,后端数据就断链。真正可维护的做法是让每个答案自己声明身份:
-
question_id必须全局唯一、不可变(推荐用短 UUID 或语义化字符串如"age_group",别用中文或题干原文) -
type字段明确定义题型:"single_choice"、"multi_choice"、"text"、"rating"等 -
value统一为字符串或数组:单选存"35-44",多选存["reading", "hiking"],填空存"建议增加夜间场次";禁止混用类型,否则$group会漏掉某些值 - 跳题时留空数组(
[])或设null,但别删整个对象——否则无法按顺序对齐题干
什么时候该拆出 answers 独立集合?
不是“问卷复杂就要拆”,而是看查询压力点。如果 80% 请求是查某次提交的完整答卷(比如导出 PDF),嵌套在 submissions 文档里最省事;但如果常跑这类聚合:
db.answers.aggregate([
{ $match: { question_id: "age_group", value: "35-44" } },
{ $group: { _id: "$submission_id", count: { $sum: 1 } } }
])
那就必须拆。原因有三:
- 独立集合能给
question_id和value建复合索引,$match速度翻倍 - 单个答案更新(比如用户修改某题)变成原子操作,不用读-改-写整份答卷
- 答卷量超百万后,
submissions文档里堆几万个answers元素,BSON 解析开销剧增,甚至触发 16MB 限制
$unwind 统计时总数不准?空数组被丢弃了
这是最隐蔽的坑。用 $unwind: "$answers" 拆数组做统计时,如果某条答卷的 answers 是空数组 [],默认行为会直接跳过该文档——导致总样本数少算。这不是 bug,是设计如此。
修复只需加参数:
db.submissions.aggregate([
{ $unwind: { path: "$answers", preserveNullAndEmptyArrays: true } },
{ $match: { "answers.question_id": "age_group" } }
])
加上 preserveNullAndEmptyArrays: true 后,空数组也会生成一条 answers: null 记录,后续 $match 可过滤掉无效项。漏掉这个参数,跨问卷统计的百分比就全偏了。
真正难的不是怎么建模,而是每次加新题型前,得同步确认:前端传的 type 值是否被后端校验覆盖、value 是否强制转成目标类型、聚合 pipeline 里有没有漏掉 preserveNullAndEmptyArrays —— 这些细节不写死在代码里,光靠文档约定,三个月后就没人记得。











