不该。嵌套数组会导致BSON大小超限、聚合统计低效且索引失效;应拆分为surveys、responses、questions三集合,答案用题目ID作字段名扁平存储,并固化快照保障历史一致性。

问卷文档该不该用嵌套数组存题目?
不该。把 questions 设为数组,每道题用对象表示,看似直观,但会快速触达 BSON 16MB 限制——尤其当填答数据(answers)也嵌在题内时。更关键的是,MongoDB 无法高效对数组内任意题目的答案做聚合统计(比如“第 7 题选‘非常同意’的人数”),$unwind 后性能陡降,且索引失效。
推荐拆成两张集合:surveys(问卷元信息)和 responses(作答记录),题目定义抽离到 questions 集合。每条 response 文档用扁平字段存答案,例如:q_12345: "strongly_agree"、q_67890: ["option_a", "option_c"]。这样既支持直接索引单题,又避免深度嵌套。
动态字段名怎么保证查询和校验不翻车?
字段名不能拼错,也不能随意增删——否则应用层查不到数据,或校验逻辑漏掉新题。必须让字段名来源唯一可信:从 questions 集合的 _id 生成,例如题目 ID 是 ObjectId("65a1b2c3d4e5f67890123456"),就用其十六进制字符串 "65a1b2c3d4e5f67890123456" 作为字段名前缀(如 q_65a1b2c3d4e5f67890123456)。
实操要点:
- 插入
response前,先查questions集合确认该题存在且未禁用(status: "active") - 应用层用白名单校验字段名:只允许
q_+ 24位十六进制字符,拒绝任何其他命名模式 - 建复合索引时显式列出高频查询题字段,例如
{ "q_65a1b2c3d4e5f67890123456": 1, "created_at": -1 },别依赖通配符索引({"$**": 1}效率低且不支持部分查询场景)
多选题、评分题、文本题的答案怎么统一存又不失类型语义?
不强求统一结构。MongoDB 的优势正在于按需存储:多选题存 Array,评分题存 Number,文本题存 String,日期题存 Date。只要字段名规则一致,上层应用能根据题目配置(从 questions 集合读取 type: "multiple_choice")决定如何解析该字段值。
但要注意两个陷阱:
- 空值处理:未作答的题字段不写入文档(而非存
null或空数组),否则统计时需额外过滤,且稀疏索引失效 - 类型混用:禁止同一题目字段存不同类型(比如有时是
String,有时是Number),这会让聚合管道$switch或应用反序列化崩溃 - 长文本题建议单独存到
response_texts集合,用response_id关联,避免撑大主文档
历史问卷修改后,旧答卷还能正确关联题目吗?
不能靠实时读取 questions 集合来渲染旧答卷——题目描述、选项可能已改,甚至被删除。必须在创建答卷时,把题目快照固化进去。
最简方案:在 responses 文档里加一个 snapshot 子文档,只存关键元数据,例如:
{
"q_65a1b2c3d4e5f67890123456": { "text": "您对服务的整体满意度?", "options": ["非常不满意", "不满意", "一般", "满意", "非常满意"] }
}
这个 snapshot 不参与查询,只用于回溯展示。体积小、写入快、与答案字段解耦。别试图把整个题目文档嵌进来——冗余太大,且违背单一数据源原则。
真正容易被忽略的是时间戳粒度:问卷发布、题目启用、答卷提交,这三个时间点要分别记录(survey.published_at、question.enabled_at、response.submitted_at),否则无法判断某次作答究竟对应哪一版题目配置。











