MongoDB技术书籍-清单-笔记建模:扁平化及重复存储是否合理?
技术书籍层级数据建模的最佳实践探讨
问题背景
我正尝试为以下层级结构建模:技术书籍→参考编号→主题领域→主题→清单→多条笔记,当前设计的数据结构及查询逻辑如下:
层级结构示例
/* technical book 1 reference number subject area topic checklist1 note1 note2 note3 note4 //many more notes checklist2 note1 note2 note3 //many more notes checklist3 note1 note2 note3 //many more notes */
现有数据模型
笔记集合(数千条数据)
[ { _id: 8172931, noteTitle: "my 1st note title", noteText: "something interesting", noteRef: "other information", belongsTo: 27, // 关联清单ID }, { _id: 8172932, noteTitle: "my 2nd note title", noteText: "something interesting", noteRef: "other information", belongsTo: 28, // 关联清单ID }, ];
清单集合(约1000条数据)
[ { _id: 27, checklistName: "1st checklist name", checklistTopic: "a topic", checklistSubject: "a subject", checklistRefNum: "f-38", checklistBookName: "the book that the checklist belongs to", }, { _id: 28, checklistName: "2nd checklist name", // 唯一值 checklistTopic: "a topic", // 重复值 checklistSubject: "a subject", // 重复值 checklistRefNum: "g-32", // 重复值 checklistBookName: "the book that the checklist belongs to", // 重复值 }, ];
当前查询逻辑
应用UI会根据用户选择的BookName、RefNum、Subject、Topic筛选清单,查询语句如下:
const agg = [ { '$match': { 'checklistBookName': 'example name', 'checklistRefNum': 'f-38', 'checklistSubject': 'a subject', 'checklistTopic': 'a topic' } } ]; const coll = client.db('databaseName').collection('checklists'); const cursor = coll.aggregate(agg); const result = await cursor.toArray();
选中清单后,再根据清单ID获取关联的所有笔记。
我的疑问是:这种扁平化层级数据并在清单集合中重复存储数据的方式是否为最佳实践?有没有更好的方案避免数据重复?
分析与解决方案
现有方案的优缺点
你的当前设计属于反规范化(Denormalization),在MongoDB这类文档数据库中,这种做法并非完全不可取,具体看场景:
- 优点:查询效率高,筛选清单时无需关联其他集合,一次
$match就能得到结果,符合你当前的查询逻辑,开发成本低。 - 缺点:数据冗余明显,书籍名、参考编号、主题领域、主题这些字段会在多个清单文档中重复存储。如果后续这些信息需要修改(比如书籍名变更),就得批量更新所有关联的清单文档,操作繁琐且容易出错。
优化方案:规范化数据模型
如果想要避免数据重复,建议拆分层级,建立独立集合存储上层数据,通过ID关联,这是**规范化(Normalization)**的思路,具体可以这样设计:
1. 书籍集合(books)
存储技术书籍的基础信息:
[ { _id: "book_1", bookName: "the book that the checklist belongs to", // 其他书籍相关字段 } ]
2. 参考编号集合(referenceNumbers)
关联书籍,存储参考编号信息:
[ { _id: "ref_f-38", refNum: "f-38", belongsToBook: "book_1" // 关联书籍ID }, { _id: "ref_g-32", refNum: "g-32", belongsToBook: "book_1" } ]
3. 主题领域集合(subjectAreas)
关联参考编号,存储主题领域信息:
[ { _id: "subject_1", subjectName: "a subject", belongsToRef: "ref_f-38" // 关联参考编号ID } ]
4. 主题集合(topics)
关联主题领域,存储主题信息:
[ { _id: "topic_1", topicName: "a topic", belongsToSubject: "subject_1" // 关联主题领域ID } ]
5. 清单集合(checklists)
仅存储清单自身信息,通过ID关联上层主题:
[ { _id: 27, checklistName: "1st checklist name", belongsToTopic: "topic_1" // 关联主题ID }, { _id: 28, checklistName: "2nd checklist name", belongsToTopic: "topic_1" } ]
6. 笔记集合(notes)
保持现有设计不变,关联清单ID:
[ { _id: 8172931, noteTitle: "my 1st note title", noteText: "something interesting", noteRef: "other information", belongsTo: 27, } ]
优化后的查询逻辑
当需要根据BookName、RefNum、Subject、Topic筛选清单时,需要通过多集合关联查询(用$lookup)来实现:
const agg = [ // 先从主题集合筛选匹配的主题 { $match: { topicName: "a topic" } }, // 关联主题领域集合,筛选匹配的主题领域 { $lookup: { from: "subjectAreas", localField: "belongsToSubject", foreignField: "_id", as: "subject" } }, { $unwind: "$subject" }, { $match: { "subject.subjectName": "a subject" } }, // 关联参考编号集合,筛选匹配的参考编号 { $lookup: { from: "referenceNumbers", localField: "subject.belongsToRef", foreignField: "_id", as: "ref" } }, { $unwind: "$ref" }, { $match: { "ref.refNum": "f-38" } }, // 关联书籍集合,筛选匹配的书籍 { $lookup: { from: "books", localField: "ref.belongsToBook", foreignField: "_id", as: "book" } }, { $unwind: "$book" }, { $match: { "book.bookName": "example name" } }, // 关联清单集合,获取对应的清单 { $lookup: { from: "checklists", localField: "_id", foreignField: "belongsToTopic", as: "checklists" } }, // 整理输出结果,只保留清单信息 { $project: { _id: 0, checklists: 1 } } ]; const coll = client.db('databaseName').collection('topics'); const cursor = coll.aggregate(agg); const result = await cursor.toArray();
方案选择建议
- 如果你的数据几乎不会变更(比如书籍、参考编号这些信息一旦确定就不会修改),那么当前的反规范化方案是可以接受的,毕竟查询更简单高效。
- 如果数据存在变更需求,或者未来可能需要扩展更多上层层级的属性,那么规范化方案更合适,能避免数据不一致的问题,维护成本更低。
- 也可以考虑混合方案:比如把书籍名和参考编号这些变更频率极低的字段保留在清单集合中,主题领域和主题拆分成独立集合,平衡查询效率和数据冗余。
内容的提问来源于stack exchange,提问作者enginedave
相关产品推荐
相关产品推荐

