应使用嵌套对象存储题目,因试卷本质是“题号→题干+选项+分值”的映射;数组导致增删重排、下标错乱、查询脱钩;嵌套对象支持原子更新、题号即字段名、路径清晰。

试卷文档该用嵌套对象还是数组存题目
用嵌套对象,别用数组。试卷本质是「题号 → 题干+选项+分值」的映射,不是按顺序排列的线性序列。用数组(如questions: [{q: "q1", text: "..."}, ...])会导致每次增删题都要重排整个数组,更新成本高、容易下标错乱;而嵌套对象(如questions: {"q1": {...}, "q42": {...}})支持原子更新单题、字段名即题号、查询路径清晰。
常见错误现象:find({ "questions.0.text": /正则/ })想查第一题题干含关键词的试卷,结果漏掉所有题号不从q1开始或中间有空缺的试卷——因为数组索引和题号逻辑脱钩。
- 题号统一加前缀(如
q1、q23),避免纯数字字段名在某些驱动中触发解析异常 - 若需按题号范围筛选(如“只取q1–q20”),用
$regex: "^q[1-9]|^q1[0-9]|^q20$",或预存question_ids: ["q1","q2",...]字段辅助查询 - 不要为“保持顺序”强行用数组——MongoDB 本身不保证对象字段顺序(虽多数驱动按插入序返回,但不可依赖),真要排序交由应用层或聚合管道处理
答题卡文档必须避免稀疏矩阵式存储
答题卡文档结构必须是{"answers": {"q1": "A", "q7": null, "q42": "D"}},而不是{"answers": ["A", null, ..., "D"]}。后者是典型稀疏矩阵思维,完全违背 MongoDB 文档模型优势。
常见错误现象:find({ "answers.49": "C" })查第50题答C的学生,结果为空——因为字段名是字符串"q50",不是数组下标49;或者前端漏传某题,后端写入时数组空位错位,导致answers[49]实际对应第51题。
- 原子更新单题只需
$set: {"answers.q23": "B"},IO小、锁粒度细;数组则每次改一题都得全量写入整个answers字段 - 索引必须建在具体路径上,如
answers.q8,通配符索引answers.$**可覆盖动态题号,但慎用——它会索引所有子字段,增加存储与写入开销 - 导出为稠密数组(如生成PDF答题卡)应在聚合阶段做:
$objectToArray转键值对 →$map提取题号并$toInt转数字 →$sort→$map提答案值,全程不改动原始数据
如何支持“随机组卷”与“题库复用”共存
试卷文档里不存完整题目内容,只存题库ID + 题号引用;答题卡文档也只存题号+作答,不存题干。题干、选项、标准答案等结构化内容统一存在questions集合,靠_id或q_id关联。
这样设计才能让同一道题被多份试卷复用,且修改题干后所有已考未阅卷的答题卡仍能正确渲染(只要后端接口查题时带版本号或时间戳控制)。
- 试卷文档示例:
{_id: ..., question_refs: [{q_id: "Q_abc123", q_no: "q1", score: 2}, ...]} - 答题卡文档示例:
{_id: ..., exam_id: "...", answers: {"q1": "A", "q2": ["X","Y"]}, submitted_at: ...} - 查某次考试完整试卷:先查试卷文档得
question_refs,再$lookup聚合到questions集合取题干;查某人某题作答,直接find({ _id: "...", "answers.q7": { $exists: true } })
哪些字段必须加索引,哪些纯属浪费
索引不是越多越好。没被find、sort、aggregate真正用上的字段,建索引只会拖慢写入、涨磁盘。
常见误操作:给answers整个子文档建索引(answers: 1),以为能加速所有题号查询——其实无效,MongoDB 不支持对动态键名的子文档做高效范围查询。
- 高频查询场景决定索引策略:
— 查“第8题选C的所有人” → 建单独索引:{"answers.q8": 1}
— 查“某人所有作答” → 对answers建哈希索引({"answers": "hashed"})或不用索引,靠_id查更快
— 查“某场考试全部答题卡” → 索引在exam_id上,别在answers上折腾 - 通配符索引
{"answers.$**": 1}仅当确实需要任意题号模糊查询(如管理员后台搜“所有q*字段含null的答题卡”)才启用,生产环境慎开 - 复合索引优先考虑过滤性强的字段前置,例如
{exam_id: 1, "answers.q12": 1}比反过来更有效
q1,后端存Q1,聚合时$objectToArray输出的key却是"Q1",排序就全乱。所有环节(表单、API、聚合、索引)必须统一用小写q前缀+纯数字,一个字符都不能松动。











