mongodb - MongoDB 统计相关集合中的数百万个文档
问题描述
所以,我被困住了,我在 Stackoverflow 上的第一颗子弹,经过多年的潜伏,我绝对需要一些好的建议。我有两种文档类型:
文章
今天大约有 15,000 篇文章,但在加入客户时迅速增加。我们不想在这里限制。
{
"_id" : ObjectId("5bd054d8fd5298d07ddc293a"),
"title" : "A neat title"
}
活动
每篇文章大约有 1k 个活动,在用户导航的每个营销相关阶段编写(例如:查看或分享文章)。为网站带来更多流量将增加文章和活动之间的 1/1000 比率。
{
"_id" : ObjectId("5bbdae8afd529871473c1111"),
"article" : ObjectId("5bd054d8fd5298d07ddc293a"),
"what" : "view"
}
{
"_id" : ObjectId("5bbdae8afd529871473c2222"),
"article" : ObjectId("5bd054d8fd5298d07ddc293a"),
"what" : "share"
}
我的目标是汇总计数相关活动的文章:
{
"_id" : ObjectId("5bd054d8fd5298d07ddc293a"),
"title" : "A neat title",
"statistics" : {
'view':1,
'share':1,
}
}
Activity.article 和 Activity.what 上的索引都设置好了。
在小型数据集上,我可以通过这种聚合轻松实现我的目标:
db.article.aggregate([
{ $match: {
... some unrelevant match
}},
{ $lookup: {
from: "activity",
localField: "_id",
foreignField: "article",
as: "activities"
}},
{ $project: {
data: '$$ROOT',
views: {$filter: {
input: '$activities',
as: 'view',
cond: {$eq: ['$$what', 'view']}
}},
shares: {$filter: {
input: '$activities',
as: 'share',
cond: {$eq: ['$$what', 'share']}
}}
}},
{ $addFields: {
'data.statistics.views': { $size: '$views' },
'data.statistics.shares': { $size: '$shares' }
}},
{ $project: {
'data.activities': 0,
'views': 0,
'shares': 0
}},
{ $replaceRoot: { newRoot: '$data' } },
])
只要 $lookup 没有超过 16MB 限制,这正是我想要的。如果我有数百万个活动,那么即使文档指出,聚合也会失败:
聚合管道限制该限制仅适用于返回的文档;在管道处理期间,文档可能会超过此大小
我已经尝试了什么:
- 添加 allowDiskUse / 失败,它似乎没有写任何东西,因为我没有在我的数据目录中看到 _tmp 文件夹
- 添加 allowDiskUse + cursor / 也失败
- 使用 { $out:"result" } / 将结果保存在临时集合中失败
- 使用Lookup+Unwind 合并更改聚合/它可以工作,但对于 150 万个活动,结果会在 10 秒内返回,因为在 Unwind 之后,管道的每个阶段(即:组回重建文档)不能使用现有索引。
- 使用内部流水线更改查找/它有效
但是对于 20 万个活动,它需要 1.5 分钟(我停止了 150 万个测试)并在 6 秒内返回结果。这可能是我最好的马...
我什至尝试过这样的事情:
db.article.aggregate([
{ $match: {
...
}},
{ $addFields: {'statistics.views': db.activity.find({ "article": ObjectId('5bd054d8fd5298d07ddc293a'), "what" : "view" }).count()
])
效果很好(0.008 秒/文章)。问题是我无法“可变”该 ObjectId:
db.article.aggregate([
{ $match: {
...
}},
{ $addFields: {
'statistics.views': db.activity.find({ "article": ObjectId('5bd054d8fd5298d07ddc293a'), "what" : "view" }).count(),
// ^ returns correct count
'statistics.querystring': { $let: {
vars: { articleid: "$_id", whatvalue: 'view' },
in: { 'query':{ $concat: [ "db.activity.find( { 'article': ObjectId('", { $toString: "$$articleid" }, "'), 'what' : '", "$$whatvalue", "' } ).count()" ] } }
}},
// ^ returns correct query to string
'statistics.variablequery': { $let: {
vars: { articleid: "$_id", whatvalue: 'view' },
in: db.activity.find( { "article": '$$articleid', "what" : "$$whatvalue" } ).count()
}},
// ^ returns 0
}}
])
我对所有解决方案持开放态度,即使我在编写活动时排除了在文章中增加计数器的可能性,也可以更改我的收藏,因为我需要按日期过滤(即:给我上周的所有份额)
解决方案
活动文档有多大?由于它们看起来很小 - 我会将活动作为数组保存在文章文档中。文档限制为 16mb,这样应该没问题,您可以避免磁盘上的 _id 和重复的文章 ID 字段 - 使磁盘上的数据更小。请记住,MongoDB 不是您的传统 SQL 数据库 - 嵌入式字段和文档是您的朋友。
如果活动将是无限的(即可以永远增长),那么我建议一种分桶方法,您每天每篇文章都有一个活动文档,例如:
{
"_id" : {
"article" : ObjectId("5bbdae8afd529871473c2222"),
"when": "2018-12-27"
},
"activities" : [
{"what": "view", "when": "12:01"},
{"what": "share", "when": "13:16"}
]
}
您可以在“何时”字段中存储完整的时间戳或 ISODates,但这种方法更具可读性,并且在磁盘上可能更紧凑。
推荐阅读
- ruby - 使用 TTS gem 在 Ruby 中运行动画
- python-3.x - 当我打印出我的功能时,有没有办法避免得到“无”?
- bash - 使用 awk 正则表达式后捕获日期
- docker - docker run --cap-add=SYS_PTRACE 获取“无效的引用格式:存储库名称必须为小写”
- java - 如何更改为我可以删除的 ValueEventListener?
- android - Flutter - AndroidX 迁移后,Linux 中的 Gradle 构建失败
- javascript - 如何检测字符串中的特殊字符:VueJS
- navigation - 我想从一个屏幕导航到另一个屏幕,但我有 undefined is not an object(评估'this.props.navigation.navigate')
- ios - 如果应用程序进入后台,如何阻止它刷新?
- ubuntu-18.04 - 在 WSL (ubuntu 18.04) 中执行“/usr/bin/spin”的权限被拒绝