问题
作为written before,过度嵌入有几个问题:
问题 1:BSON 大小限制
截至撰写本文时,BSON documents are limited to 16MB。如果达到该限制,MongoDB 将抛出异常,您根本无法添加更多 cmets,在最坏的情况下,如果更改会增加文档的大小,甚至不会更改(用户)名称或图片。
问题 2:查询限制和性能
在某些条件下,对 cmets 数组进行查询或排序并不容易。有些事情需要相当昂贵的聚合,有些则需要相当复杂的语句。
虽然有人可能会争辩说,一旦查询到位,这不是什么大问题,但我不敢苟同。首先,查询越复杂,对开发人员和后续 MongoDB 查询优化器的优化就越困难。我通过简化数据模型和查询获得了最好的结果,在一个实例中将响应速度提高了 100 倍。
在扩展时,与更简单的数据模型和相应的查询相比,复杂和/或昂贵的查询所需的资源甚至可能总计为整台机器。
问题 3:可维护性
最后但并非最不重要的一点是,您很可能会在维护代码时遇到问题。作为一个简单的经验法则
您的代码越复杂,就越难维护。代码越难维护,维护代码所需的时间就越多。维护代码所需的时间越多,成本就越高。
结论:复杂的代码很昂贵。
在这种情况下,“昂贵”既指金钱(用于专业项目),也指时间(用于业余项目)。
(我的!)解决方案
这很简单:简化您的数据模型。因此,您的查询将变得不那么复杂并且(希望)更快。
第 1 步:确定您的用例
这对我来说将是一个疯狂的猜测,但这里重要的是向您展示一般方法。我将您的用例定义如下:
- 对于给定的帖子,用户应该能够发表评论
- 对于给定的帖子,显示作者和 cmets,以及评论者和作者的用户名和他们的照片
- 对于给定的用户,应该可以轻松更改名称、用户名和图片
第 2 步:对数据进行相应建模
用户
首先,我们有一个简单的用户模型
{
_id: new ObjectId(),
name: "Joe Average",
username: "HotGrrrl96",
picture: "some_link"
}
这里没有新内容,只是为了完整而添加。
帖子
{
_id: new ObjectId()
title: "A post",
content: " Interesting stuff",
picture: "some_link",
created: new ISODate(),
author: {
username: "HotGrrrl96",
picture: "some_link"
}
}
这就是帖子的内容。这里有两点需要注意:首先,我们存储显示帖子时立即需要的作者数据,因为这样可以为我们节省查询一个非常常见的用例,如果不是普遍存在的话。我们为什么不相应地保存 cmets 和 commenters 数据呢?由于16 MB size limit,我们试图阻止将引用存储在单个文档中。相反,我们将引用存储在注释文档中:
评论
{
_id: new ObjectId(),
post: someObjectId,
created: new ISODate(),
commenter: {
username: "FooBar",
picture: "some_link"
},
comment: "Awesome!"
}
与帖子一样,我们拥有显示帖子所需的所有数据。
查询
我们现在取得的成就是,我们绕过了 BSON 大小限制,并且我们不需要参考用户数据即可显示帖子和 cmets,这应该可以为我们节省大量查询。但让我们回到用例和更多查询
添加评论
现在完全简单了。
获取给定帖子的全部或部分 cmets
适用于所有 cmets
db.comments.find({post:objectIdOfPost})
最新的 3 个 cmets
db.comments.find({post:objectIdOfPost}).sort({created:-1}).limit(3)
因此,为了显示帖子及其所有(或部分)cmets,包括用户名和图片,我们在两个查询中。比你以前需要的多,但我们绕过了大小限制,基本上你可以为每个帖子拥有无限数量的 cmets。但是让我们来点真实的东西
获取最新的 5 个帖子及其最新的 3 个 cmets
这是一个两步过程。然而,通过适当的索引(稍后会回到),这仍然应该很快(因此可以节省资源):
var posts = db.posts.find().sort({created:-1}).limit(5)
posts.forEach(
function(post) {
doSomethingWith(post);
var comments = db.comments.find({"post":post._id}).sort("created":-1).limit(3);
doSomethingElseWith(comments);
}
)
获取给定用户的所有帖子,从最新到最旧以及他们的 cmets 排序
var posts = db.posts.find({"author.username": "HotGrrrl96"},{_id:1}).sort({"created":-1});
var postIds = [];
posts.forEach(
function(post){
postIds.push(post._id);
}
)
var comments = db.comments.find({post: {$in: postIds}}).sort({post:1, created:-1});
请注意,我们这里只有两个查询。虽然您需要“手动”在帖子和它们各自的 cmets 之间建立连接,但这应该非常简单。
更改用户名
这大概是一个很少执行的用例。但是,上述数据模型并不是很复杂
首先,我们更改用户文档
db.users.update(
{ username: "HotGrrrl96"},
{
$set: { username: "Joe Cool"},
$push: {oldUsernames: "HotGrrrl96" }
},
{
writeConcern: {w: "majority"}
}
);
我们将旧用户名推送到相应的数组。这是一种安全措施,以防以下操作出现问题。此外,我们将写入关注度设置为相当高的级别,以确保数据是持久的。
db.posts.update(
{ "author.username": "HotGrrrl96"},
{ $set:{ "author.username": "Joe Cool"} },
{
multi:true,
writeConcern: {w:"majority"}
}
)
这里没什么特别的。 cmets 的更新语句看起来几乎相同。虽然这些查询需要一些时间,但它们很少被执行。
索引
根据经验,可以说 MongoDB 每个查询只能使用一个索引。虽然这并不完全正确,因为存在索引交叉点,但很容易处理。另一件事是复合索引中的各个字段可以独立使用。因此,一种简单的索引优化方法是找到在使用索引的操作中使用的字段最多的查询,并为它们创建一个复合索引。请注意,查询中的出现顺序很重要。所以,让我们继续吧。
帖子
db.posts.createIndex({"author.username":1,"created":-1})
评论
db.comments.createIndex({"post":1, "created":-1})
结论
无可否认,每个帖子完全嵌入文档是加载它的最快方式,它是 cmets。但是,它不能很好地扩展,并且由于处理它所必需的可能复杂查询的性质,这种性能优势可能会被利用甚至消除。
使用上述解决方案,您可以用一些速度(如果!)来换取基本上无限的可扩展性和更直接的数据处理方式。
Hth.