在MongoDB中查找共享键值的两个文档

5

我在MongoDB中有一个大的文档集合,每个文档都有一个叫做“name”的关键字和另一个叫做“type”的关键字。我想要查找两个具有相同名称但不同类型的文档,这是一个简单的MongoDB查询:

SELECT ...
FROM table AS t1, table AS t2
WHERE t1.name = t2.name AND t1.type <> t2.type

我可以想象使用聚合功能来实现此目的,但是集合非常大,处理它需要时间,而我只是寻找一对这样的文件。

它们是两个不同的集合吗?- $or 是运算符 - http://docs.mongodb.org/manual/reference/operator/query/or/ - 你的问题中 SQL 的示例提示它们是 2 个集合,但你的开头说“一个大型文档集合”,意味着一个集合。 - Rob Sedgwick
@RobSedgwick:我不知道名称应该具有哪个值,我只知道它应该被两个不同的文档共享。我能否在“namevalue”中指示变量?如何说明类型值是不同的? - Alexander Serebrenik
所有这些操作似乎都假定可以基于文档本身确定真值(即文档是否应包含在结果中),但对于我正在尝试编写的查询来说并非如此。 - Alexander Serebrenik
find查询中,您无法同时访问多个文档,因此必须使用aggregate(可能)或mapReduce来完成此操作。但是,这不会像简单或快速的Mongo一样适合此类查询。 - JohnnyHK
@RobSedgwick:不,数据库只是有一个叫做“type”的键,与BSON类型无关。 - Alexander Serebrenik
显示剩余7条评论
2个回答

3

虽然我坚持认为你提出的问题并没有与你实际遇到的问题有关,但我会尝试解释SQL习惯用法在MongoDB解决方案中的应用。我坚信你实际的解决方案可能是不同的,但你目前只提供了SQL相关的内容。

请考虑以下文档作为样本集,为了清晰起见,在此列表中删除_id字段:

{ "name" : "a", "type" : "b" }
{ "name" : "a", "type" : "c" }
{ "name" : "b", "type" : "c" }
{ "name" : "b", "type" : "a" }
{ "name" : "a", "type" : "b" }
{ "name" : "b", "type" : "c" }
{ "name" : "f", "type" : "e" }
{ "name" : "z", "type" : "z" }
{ "name" : "z", "type" : "z" }

如果我们在相同的数据上运行所提供的SQL语句,将获得以下结果:
a|b
a|c
a|c
b|c
b|a
b|a
a|b
b|c

我们可以看到有2个文档不匹配,然后推导出SQL操作的逻辑。因此,另一种说法是:“在键“name”给定的情况下,具有键“type”中多于一个可能值的文档是哪些。”
鉴于此,采用Mongo方法,我们可以查询不符合给定条件的项目。因此,有效地得到相反的结果:
db.sample.aggregate([

    // Store unique documents grouped by the "name"
    {$group: { 
        _id: "$name",
        comp: {
            $addToSet: { 
                name:"$name",
                type: "$type" 
            }
        } 
    }},

    // Unwind the "set" results
    {$unwind: "$comp"},

    // Push the results back to get the unique count
    // *note* you could not have done this with alongside $addtoSet
    {$group: {
        _id: "$_id",
        comp: {
            $push: { 
                name: "$comp.name",
                type: "$comp.type" 
            }
        },
        count: {$sum: 1} 
    }},

    // Match only what was counted once
    {$match: {count: 1}},

    // Unwind the array
    {$unwind: "$comp"},

    // Clean up to "name" and "type" only
    {$project: { _id: 0, name: "$comp.name", type: "$comp.type"}}

])

这个操作将会产生以下结果:
{ "name" : "f", "type" : "e" }
{ "name" : "z", "type" : "z" }

现在为了得到与SQL查询相同的结果,我们需要将这些结果导入到另一个查询中:

db.sample.find({$nor: [{ name: "f", type: "e"},{ name: "z", type: "z"}] })

最终匹配结果如下:

{ "name" : "a", "type" : "b" }
{ "name" : "a", "type" : "c" }
{ "name" : "b", "type" : "c" }
{ "name" : "b", "type" : "a" }
{ "name" : "a", "type" : "b" }
{ "name" : "b", "type" : "c" }

因此,这将起作用,但可能会使实际应用不切实际的是文档数量非常大时,我们达到了将这些结果压缩到数组的工作限制。

它也有一些问题,使用最终查找操作中的否定,这将强制扫描集合。但公正地说,SQL查询使用相同的否定前提也可以这样说。

编辑

当然,我没有提到的是,如果结果集反过来并且您正在从聚合的排除项中匹配更多结果,则只需反转逻辑以获取所需的键即可。只需按以下方式更改$match:

{$match: {$gt: 1}}

这将是结果,也许不是实际的文档,但它是一个结果。因此您不需要再次查询以匹配负面案例。

最终,这是我的错,因为我太专注于习惯用语的翻译,没有阅读您问题中的最后一行,在那里您确实要求只返回一个文档。

当然,当前如果结果大小大于16MB,则会出现问题。至少在2.6版本之前是这样的,聚合操作的结果是一个游标,所以您可以像使用.find()一样迭代游标。

2.6中引入了$size运算符,用于查找文档中数组的大小。因此,这将有助于消除第二个$unwind$group,这些操作用于获取集合的长度。这将把查询转换为更快的形式:

db.sample.aggregate([
    {$group: { 
        _id: "$name",
        comp: {
            $addToSet: { 
                name:"$name",
                type: "$type"
            }
        } 
    }},
    {$project: { 
        comp: 1,
        count: {$size: "$comp"} 
    }},
    {$match: {count: {$gt: 1}}},
    {$unwind: "$comp"},
    {$project: { _id: 0, name: "$comp.name", type: "$comp.type"}}
])

如果您仅用于个人使用或开发/测试,目前可使用 MongoDB 2.6.0-rc0。


故事的寓意是:是的,您可以这样做,但您真的想要或者需要以这种方式进行吗?也许不是,如果您问一个关于具体业务案例的不同问题,您可能会得到不同的答案。但另一方面,这可能恰好是您想要的。

注意

值得一提的是,当您查看来自 SQL 的结果时,如果您没有对这些值使用 DISTINCT 或者其他分组方式,它将由于其他可用类型选项而错误地重复多个项目。但这就是使用 MongoDB 进行此过程产生的结果。

针对 Alexander

以下是当前 2.4.x 版本的 shell 聚合输出:

{
    "result" : [
            {
                    "name" : "f",
                    "type" : "e"
            },
            {
                    "name" : "z",
                    "type" : "z"
            }
    ],
    "ok" : 1
}

操作步骤如下,将变量作为参数传递给第二个 find 中的 $nor 条件:

var cond = db.sample.aggregate([ .....

db.sample.find({$nor: cond.result })

您应该获得相同的结果。否则,请咨询您的驱动程序。


是的,这正是我真正想做的。我运行了查询的第一部分并得到了很多结果,太多了,无法将它们复制并粘贴到$nor中。我能否在第二个查询中嵌套第一个查询,或者应该将第一个查询的结果存储在文件中? - Alexander Serebrenik
@AlexanderSerebrenik,我的错。我习惯于在2.6 shell中工作。Aggregate将为shell提供一个数组结果,但您需要稍微调整一下才能获得结果值。应该在results下作为一个键。 - Neil Lunn
@AlexanderSerebrenik 在结尾添加了一个部分,以说明从聚合响应传递到下一个查询的情况。希望这有所帮助。 - Neil Lunn
@NeilLunn 我第一次运行你的查询时犯了一个愚蠢的错误;现在结果显示聚合超过了16MB,所以我甚至无法继续进行。 - Alexander Serebrenik
@AlexanderSerebrenik 看一下修改。按照所示的方式反转逻辑。但这是我的警告。我们对你的问题除了解决习惯用法的SQL到Mongo问题之外,无法做更多的事情。所以如果这不适合你,最好按照我指出的方法,在另一个问题中提供你的用例,可能会有更好的解决方案,比如可能的重新建模。否则,如果这只是一个一次性操作,请将集合转储到你的SQL数据库中并在那里处理。 - Neil Lunn
显示剩余2条评论

3

有一个非常简单的聚合方法可以帮您获取出现多次的名称及其类型:

db.collection.aggregate([
      { $group: { _id : "$name", 
        count:{$sum:1},
        types:{$addToSet:"$type"}}},
      {$match:{"types.1":{$exists:true}}}
])

这适用于支持聚合框架的所有版本。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接