必须用单向引用+中间集合article_tags,而非articles中存tags数组;因数组无法按时间排序分页、不支持字段级权限校验、难建高效复合索引、标签变更引发写放大,且中间集合需用objectid字段、建必要复合索引并严格管控删除流程。

直接说结论:文章与标签之间必须用单向引用 + 中间集合,别存数组,也别双向维护 ID。哪怕当前业务看起来“只是打几个标签”,只要未来可能查“某标签下最近 10 篇文章”或“用户上周新增的收藏标签”,数组引用就立刻失效。
为什么不能在 articles 里直接存 tags 数组?
表面看省事:{ title: "MongoDB建模", tags: ["mongodb", "nosql", "performance"] }。但问题藏在细节里:
- 无法按“添加时间”排序或分页——你没法给字符串数组里的每个元素记时间戳
- 无法做权限控制——比如“仅管理员可为文章打
private标签”,数组字段本身不支持字段级校验 - 无法建有效索引支持复合查询——
db.articles.find({ "tags": "mongodb", createdAt: { $gt: ... } })会扫全表,除非你用多键索引,但那会指数级膨胀索引体积 - 标签名变更时要批量更新所有文章文档,写放大严重
中间集合 article_tags 怎么设计才不踩坑?
它不是“过度设计”,而是把“谁在什么时候打了什么标签”这个事实显式落地。字段精简到只留必要信息:
-
article_id:存ObjectId类型,不是字符串;否则db.article_tags.createIndex({ article_id: 1 })白建 -
tag_id:同理,必须是原生ObjectId -
created_at:带时区的Date,别用字符串或毫秒数 -
user_id(可选):如果需要追溯是谁打的标签
必建复合索引:db.article_tags.createIndex({ tag_id: 1, created_at: -1 }) 支持“查某标签下最新文章”;db.article_tags.createIndex({ article_id: 1 }) 支持反查文章所有标签。
查询时该用 $lookup 还是应用层两查?
取决于场景:
- 查单篇文章详情(含标签名):用
$lookup,但必须加$match先过滤文章,再$lookup关联tags集合,最后$project只取name字段——避免把整个tag文档拖进来 - 查“标签 A 下最近 20 篇文章”:别硬套
$lookup。先查article_tags拿到article_id列表,再find({ _id: { $in: [...] } })查文章主文档。这样能走索引、可控分页、易加缓存 - 聚合统计(如“各标签文章数”):直接对
article_tags聚合,group: { _id: "$tag_id" }, count: { $sum: 1 } },比从articles数组里$unwind快得多且稳定
真正容易被忽略的是:中间集合一旦建了,tags 集合本身就不能再删了——因为 article_tags.tag_id 是裸引用,MongoDB 不做外键约束。删标签前,必须先删对应的所有 article_tags 记录,否则留下脏数据,后续查“某标签文章数”就会不准。











