【问题标题】:how do I do 'not-in' operation in mongodb?如何在 mongodb 中进行“不在”操作?
【发布时间】:2012-06-08 00:58:21
【问题描述】:

我有两个系列 - 购物者(特定日期店内的每个人)和海滩游客(特定日期海滩上的每个人)。每天都有条目,人们可以在海滩上,或者购物,或者两者都做,或者任何一天都不做。我现在想查询 - 过去 7 天内没有去海滩的所有购物者。

我是 Mongo 的新手,所以我的架构设计可能不适合 nosql 数据库。我在 join 上看到了类似的问题,在大多数情况下,建议进行非规范化。因此,我能想到的一种解决方案是创建集合 - 活动、日期索引、嵌入用户的操作。所以像

{
   user_id
   date
   actions {
      [action_type, ..]
   }
}

现在插入变得昂贵,因为现在我必须在插入之前进行查询。

【问题讨论】:

  • 为什么说插入前要查询?查询什么?
  • 你还知道 $nin 运算符吗? (“不在”)
  • @AsyaKamsky 我认为 Op 意味着,要插入一个动作,首先需要查询正确的用户。
  • 如果我看到一个新动作说“在海滩上”,我需要找到 user_id 的记录和日期,以便将动作嵌入到现有数据中。
  • 如果将该操作插入到正确的用户文档中,那将是一个好主意 :) 但似乎这是不可避免的

标签: mongodb database-schema schema-design


【解决方案1】:

一些建议。

找出您将要运行的所有查询,以及您需要存储的所有数据类型。例如,您是否希望在未来增加活动,或者海滩和购物就是全部?

考虑一下您将拥有多少写入和读取,哪些必须更快。

确定您的文档将如何随着时间的推移而增长,以确保您的架构长期可扩展。

这是一种可能的方法,如果您只进行这两项活动的话。每位用户每天一条记录。

{ user: "user1",
  date: "2012-12-01",
  shopped: 0,
  beached: 1
}

现在,无论您有两个还是十个活动,您的查询都变得更加简单。

当有新活动出现时,您总是必须根据它更新正确的记录。 如果您认为您可以将一条记录附加到您的集合中,指示用户、日期、活动,那么您的插入会快得多,但您的查询现在必须为用户、日期和做大量的查询工作活动。

对于建议的架构,这里是插入/更新语句:

db.coll.update({"user":"username", "date": "somedate"}, {"shopped":{$inc:1}}, true)

这就是说:“对于某个日期的用户名,将他们的购物属性增加 1,如果它不存在,则创建它,也就是 "upsert"(这是最后一个 'true' 参数)。

这里查询的是在特定日期多次进行活动 1 但未进行任何活动 2 的所有用户。

db.coll.find({"date":"somedate","shopped":0,"danced":{$gt:1}})

在选择单个文档可以连续无限增长的架构时要小心。

例如,将所有内容存储在日期和活动数组不断增长的用户集合中会遇到此问题。请参阅突出显示的部分here 对此进行解释 - 请记住,大型文档将不断进入您的工作数据集中,如果它们很大并且其中包含大量无用(旧)数据,则会损害性能您的应用程序,以及磁盘上的数据碎片。

请记住,您不必将所有数据放入一个集合中。最好有一个用户集合,其中包含该用户的一组固定属性,您可以在其中跟踪他们有多少朋友或关于他们的其他半稳定信息,并且还有一个 user_activity 集合,您可以在其中添加每个用户每天的记录什么他们所做的活动。数据的数量或规范化或非规范化与您将在其上运行的查询类型紧密相关,这就是为什么弄清楚这些是什么是我提出的第一个建议。

【讨论】:

  • 有超过 2 个动作,加上用户本身随着时间的推移而变化,所以当我为每个动作做新记录时,我也用它转储当前用户属性以便能够查询是否用户所做的操作与用户属性(例如“他有多少朋友”)之间存在任何关联。
  • 用户每个日期可以有两个以上的活动计数器。您确实希望每个操作都有新记录,您只需更改(增加)该特定日期的用户记录。我将添加一个示例更新。
  • 嗯,我可能可以保留两个计数器以便于查询,然后记录每个操作以执行每个操作的更多详细信息,例如 shopped_at。
  • @Shekhar 是的!现在您正在考虑 noSQL 方式!根据需要进行预计算/预聚合,以保持您的查询快速,但可以灵活地保留原始数据以供将来的其他查询使用:)
【解决方案2】:

现在插入变得昂贵,因为现在我必须在插入之前进行查询。

请记住,即使使用 RDBMS,当表上有索引时(即通常),插入也可能(相对)昂贵。我不认为在 Mongo 中使用嵌入式文档在这方面有很大不同。

对于查询,正如 Asya Kamsky 建议的那样,您可以使用 $nin operator 查找没有去海滩的每个人。例如:

db.people.find({ 
    actions: { $nin: ["beach"] }
});

不过,在这种情况下,使用嵌入式文档可能不是最好的方法。我认为最好的方法是拥有一个包含如下文档的“平面”活动集合:

{
    user_id
    date
    action
}

然后你可以运行这样的查询:

var start = new Date(2012, 6, 3);
var end = new Date(2012, 5, 27);
db.activities.find({ 
    date: {$gte: start, $lt: end }, 
    action: { $in: ["beach", "shopping" ] } 
});

最后一步是在您的客户端驱动程序上,查找存在“购物”记录但不存在“海滩”活动记录的用户 ID。

【讨论】:

  • 通常如果我在 RDBMS 上进行这样的插入,我会进行批量上传,所以是的,成本是存在的,但我可以基于分组更新进行优化。此外,如果查询变得稍微复杂一些会发生什么 - 查找过去 3 天内只去海滩一次的用户。
  • @Shekhar 确实变得更复杂了。我认为这可能涉及 map-reduce 查询,具体取决于您最终设计架构的方式。
  • 如果您使用的架构只是为特定日期的用户/活动增加一个计数器,我认为您不需要 map/reduce。
【解决方案3】:

一种可能的结构是使用嵌入的文档数组(users 集合):

{
    user_id: 1234,
    actions: [ 
        { action_type: "beach", date: "6/1/2012" },
        { action_type: "shopping", date: "6/2/2012" }
    ]
},
{ another user }

然后你可以做这样的查询,使用$elemMatch 来查找符合特定条件的用户(在这种情况下,是最近三天购物的人:

var start = new Date(2012, 6, 1);
db.people.find( { 
    actions : { 
        $elemMatch : { 
            action_type : { $in: ["shopping"] }, 
            date : { $gt : start } 
        } 
    } 
});

对此进行扩展,您可以使用 $and 运算符查找所有过去三天内购物但没有去海滩的人:

var start = new Date(2012, 6, 1);
db.people.find( {  
    $and: [
        actions : { 
            $elemMatch : { 
                action_type : { $in: ["shopping"] }, 
                date : { $gt : start } 
            } 
        },
        actions : { 
            $not: {
                $elemMatch : { 
                    action_type : { $in: ["beach"] }, 
                    date : { $gt : start } 
                } 
            }
        }
    ]
});

【讨论】:

  • 我不建议这样做,因为不断增长的文档会出现性能问题。最好添加文档并查询最新的文档,而不是不断增长一组文档。
  • 看起来这可能有效,因为我还可以转储每个日操作对的用户属性,以围绕“哪种用户更有可能执行此操作”进行其他查询。有多少文档嵌入到查询性能中是否重要?我可能有数百万个用户 ID,每个用户每天都有 30 个操作。 @AsyaKamsky 是的,这正是我的问题——我担心这可能会出现性能问题。
  • @AsyaKamsky 嵌入字段的索引呢? mongodb.org/display/DOCS/…
  • @Shekhar 我认为您可以为这种模式和查询设置索引。见这里:mongodb.org/display/DOCS/Multikeys
  • 问题是随着时间的推移,数组中将包含非常旧的(并且可能是陈旧和不必要的)数据。您将不断添加新日期,但每次从磁盘获取更大的特定文档时,旧数据都会不断被拉入内存。想象一下这个集合在六个月内 - 每个用户文档从第一天的一个或两个数组元素增长到数百个。每次文档增长时,mongo 都必须重新定位它,因此现在您的数据也会在磁盘上高度分散。这是一个很好的讨论,所以我会在我的答案中添加一些我的 cmets。
猜你喜欢
  • 2012-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-31
  • 2013-11-18
  • 1970-01-01
相关资源
最近更新 更多