很多社交类的应用程序都需要链接人、内容、粉丝、好友,以及其他一些事物。对于这些高度关联的数据使用内嵌的形式还是引用的形式不容易权衡。这一节会介绍社交图谱数据相关的注意事项。通常,关注、好友或者收藏可以简化为一个发布、订阅系统:一个用户可以订阅另一个用户相关的通知。这样,有两个基本操作需要比较高效:如何保存订阅者,如何将一个事件通知给所有订阅者。
比较常见的订阅实现方式有三种。第一种方式是将内容生产者内嵌在订阅者文档中:
{
"_id": ObjectId("..."),
"username": "batman",
"email": "batman@waynetech.com",
"following": [
ObjectId("..."),
ObjectId("...")
]
}
现在,对于一个给定的用户文档,可以使用形如db.activities.find({"user": {"$in": user["following"]}})
的方式查询该用户感兴趣的所有活动信息。但是,对于一条刚刚发布的活动信息,如果要找出对这条信息感兴趣的所有用户,就不得不查询所有用户的“following”字段了。
另一种方式是将订阅者内嵌到生产者文档中:
{
"_id": ObjectId("..."),
"username": "joker",
"email": "joker@mailinator.com",
"followers": [
ObjectId("..."),
ObjectId("..."),
ObjectId("...")
]
}
当这个生产者新发布一条信息时,我们立即就可以知道需要给哪些用户发布通知。这样做的缺点时,如果需要找到一个用户关注的用户列表,就必须查询整个用户集合。这样方式的优缺点与第一种方式的优缺点恰好相反。
同时,这两种方式都存在另一个问题:它们会使用户文档变得越来越大,改变也越来越频繁。通常,“following”和“followers”字段甚至不需要返回:查询粉丝列表有多频繁?如果用户比较频繁地关注某些人或者对一些人取消关注,也会导致大量的碎片。因此,最后的方案对数据进一步范式化,将订阅信息保存在单独的集合中,以避免这些缺点。进行这种成都的范式化可能有点儿过了,但是对于经常发生变化而且不需要与文档其他字段一起返回的字段,这非常有用。对“followers”字段做这种范式化使有意义的。
用一个集合来保存发布者和订阅者的关系,其中的文档结构可能如下所示:
{
"_id": ObjectId("..."), //被关注者的"_id"
"followers": [
ObjectId("..."),
ObjectId("..."),
ObjectId("...")
]
}
这样可以使用户文档比较精简,但是需要额外的查询才能得到粉丝列表。由于“followers”数组的大小经常会发生变化,所以可以在这个集合上启用“usePowerOf2Sizes”,以保证users集合尽可能小。如果将followers集合保存在另一个数据库中,也可以在不过多影响users集合的前提下对其进行压缩。
不管使用什么样的策略,内嵌字段只能在子文档或者引用数量不是特别大的情况下有效发挥作用。对于比较有名的用户,可能会导致用于保存粉丝列表的文档溢出。对于这种情况的一种解决方案使在必要时使用“连续的”文档。例如:
> db.users.find({"username": "wil"})
{
"_id": ObjectId("..."),
"username": "wil",
"email": "wil@example.com",
"tbc": [
ObjectId("123"), // just for example
ObjectId("456") // same as above
],
"followers": [
ObjectId("..."),
ObjectId("..."),
ObjectId("..."),
...
]
}
{
"_id": ObjectId("123"),
"followers": [
ObjectId("..."),
ObjectId("..."),
ObjectId("..."),
...
]
}
{
"_id": ObjectId("456"),
"followers": [
ObjectId("..."),
ObjectId("..."),
ObjectId("..."),
...
]
}
对于这种情况,需要在应用程序中添加从“tbc”(to be continued)数组中取数据的相关逻辑。
No silver bullet.
原文地址:MongoDB中的范式与反范式, 感谢原作者分享。