如何在MongoDB中执行等效的SQL Join?


498

如何在MongoDB中执行等效的SQL Join?

例如,说您有两个集合(用户和评论),我想提取所有pid = 444的评论以及每个用户的信息。

comments
  { uid:12345, pid:444, comment="blah" }
  { uid:12345, pid:888, comment="asdf" }
  { uid:99999, pid:444, comment="qwer" }

users
  { uid:12345, name:"john" }
  { uid:99999, name:"mia"  }

有没有一种方法可以一次性提取所有带有特定字段的评论(例如... find({pid:444})),以及与每个评论关联的用户信息?

此刻,我首先要获取符合我的条件的注释,然后找出该结果集中的所有uid,获取用户对象,并将它们与注释结果合并。好像我做错了。


35
关于此问题的最后一个答案可能是最相关的,因为MongoDB 3.2+实施了名为$ lookup的联接解决方案。我以为我会把它推到这里,因为也许不是每个人都会读到底部。stackoverflow.com/a/33511166/2593330
thefourtheye

6
正确的,$ lookup是在MongoDB 3.2中引入的。有关详细信息,请访问docs.mongodb.org/master/reference/operator/aggregation/lookup/…–
NDB

Answers:


306

从Mongo 3.2开始,这个问题的答案几乎不再正确。添加到聚合管道中的新$ lookup运算符实质上与左外部联接相同:

https://docs.mongodb.org/master/reference/operator/aggregation/lookup/#pipe._S_lookup

从文档:

{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

当然,Mongo 不是关系数据库,开发人员会谨慎推荐$ lookup的特定用例,但是至少从3.2版本开始,MongoDB现在可以进行联接。


@clayton:那么两个以上的集合呢?
Dipen Dedania '16

1
@DipenDedania只需在聚合管道中添加其他$ lookup阶段即可。
Clayton Gulick

我无法将左侧集合中数组的任何字段与右侧集合中的相应ID联接起来。有人可以帮助我吗?
Prateek Singh

1
我对此有些困惑-是否有任何方法可以指定您只希望“发件人”集合中的某些文档,还是可以一次将所有文档自动加入数据库?
user3413723

只是想知道最新的Spring Data MongoDB是否支持3.2?
gtiwari333 '16

142

mongodb官方站点上的此页面完全解决这个问题:

https://mongodb-documentation.readthedocs.io/zh-CN/latest/ecosystem/tutorial/model-data-for-ruby-on-rails.html

在显示故事列表时,我们需要显示发布故事的用户名。如果使用关系数据库,则可以对用户和商店执行联接,并在单个查询中获得所有对象。但是MongoDB不支持联接,因此有时需要一些非规范化。在这里,这意味着缓存“用户名”属性。

关系纯粹主义者可能已经感到不安,好像我们在违反某些普遍法律。但请记住,MongoDB集合不等同于关系表;每个都具有独特的设计目标。标准化表提供了原子的,隔离的数据块。但是,文档更紧密地代表了整个对象。就社交新闻网站而言,可以说用户名是所发布故事的固有内容。


51
@dudelgrincen是规范化和关系数据库的范式转变。NoSQL的目标是非常快速地从数据库读取和写入。借助BigData,您将拥有大量的应用程序和前端服务器,而DB上的数量则更少。您应该每秒完成数百万笔交易。从数据库中卸载繁重的工作,然后将其放到应用程序级别。如果需要深度分析,请运行一个集成作业,将数据放入OLAP数据库。无论如何,您都不应该从OLTP数据库中获得许多深度查询。
13年

18
@dudelgrincen我也应该说不是每个项目或设计都适用。如果您有在SQL类型数据库中可用的内容,为什么要更改它?如果您无法通过模式来使用noSQL,那么就不要使用。
2013年

9
在NoSQL系统上,迁移和不断发展的模式也更容易管理。
贾斯汀2014年

14
如果用户在网站上有3.540个帖子,并且确实在个人资料中更改了用户名怎么办?是否应使用新的用户名更新每个帖子?
伊沃·佩雷拉

2
@IvoPereira是的,这就是为什么应该避免以这种方式对数据建模的原因。有一篇文章解释了相同的场景及其后果:为什么永远不应该使用MongoDB
Omid

138

使用mongodb客户端控制台,我们可以使用简单的功能在几行中合并/合并仅一个集合中的所有数据,现在我们可以执行所需的查询。下面是一个完整的示例,

.-作者:

db.authors.insert([
    {
        _id: 'a1',
        name: { first: 'orlando', last: 'becerra' },
        age: 27
    },
    {
        _id: 'a2',
        name: { first: 'mayra', last: 'sanchez' },
        age: 21
    }
]);

.-类别:

db.categories.insert([
    {
        _id: 'c1',
        name: 'sci-fi'
    },
    {
        _id: 'c2',
        name: 'romance'
    }
]);

.-书籍

db.books.insert([
    {
        _id: 'b1',
        name: 'Groovy Book',
        category: 'c1',
        authors: ['a1']
    },
    {
        _id: 'b2',
        name: 'Java Book',
        category: 'c2',
        authors: ['a1','a2']
    },
]);

.-图书借阅

db.lendings.insert([
    {
        _id: 'l1',
        book: 'b1',
        date: new Date('01/01/11'),
        lendingBy: 'jose'
    },
    {
        _id: 'l2',
        book: 'b1',
        date: new Date('02/02/12'),
        lendingBy: 'maria'
    }
]);

。- 魔术:

db.books.find().forEach(
    function (newBook) {
        newBook.category = db.categories.findOne( { "_id": newBook.category } );
        newBook.lendings = db.lendings.find( { "book": newBook._id  } ).toArray();
        newBook.authors = db.authors.find( { "_id": { $in: newBook.authors }  } ).toArray();
        db.booksReloaded.insert(newBook);
    }
);

.-获取新的收集数据:

db.booksReloaded.find().pretty()

.-响应:)

{
    "_id" : "b1",
    "name" : "Groovy Book",
    "category" : {
        "_id" : "c1",
        "name" : "sci-fi"
    },
    "authors" : [
        {
            "_id" : "a1",
            "name" : {
                "first" : "orlando",
                "last" : "becerra"
            },
            "age" : 27
        }
    ],
    "lendings" : [
        {
            "_id" : "l1",
            "book" : "b1",
            "date" : ISODate("2011-01-01T00:00:00Z"),
            "lendingBy" : "jose"
        },
        {
            "_id" : "l2",
            "book" : "b1",
            "date" : ISODate("2012-02-02T00:00:00Z"),
            "lendingBy" : "maria"
        }
    ]
}
{
    "_id" : "b2",
    "name" : "Java Book",
    "category" : {
        "_id" : "c2",
        "name" : "romance"
    },
    "authors" : [
        {
            "_id" : "a1",
            "name" : {
                "first" : "orlando",
                "last" : "becerra"
            },
            "age" : 27
        },
        {
            "_id" : "a2",
            "name" : {
                "first" : "mayra",
                "last" : "sanchez"
            },
            "age" : 21
        }
    ],
    "lendings" : [ ]
}

我希望这条线可以帮助您。


2
我想知道是否可以使用doctrine mongodb运行相同的代码?
2014年

4
当一个引用对象得到更新时会发生什么?该更新会自动反映在book对象中吗?还是该循环需要再次运行?
balupton 2014年

14
只要您的数据很小,就可以了。它将把每本书的内容带给您的客户,然后逐一获取每个类别,出借和作者。当您的书籍成千上万的时候,这真的会非常缓慢。更好的技术可能是使用聚合管道并将合并的数据输出到单独的集合中。让我再回到它。我将添加一个答案。
Sandeep Giri 2014年

您可以将算法修改为其他示例吗?stackoverflow.com/q/32718079/287948
Peter Krauss

1
@SandeepGiri我应该怎么做聚合管道,因为我在分离的集合中确实有非常密集的数据需要加入?
Yassine Abdul-Rahman 2015年

38

您必须按照描述的方式进行操作。MongoDB是非关系数据库,不支持联接。


4
似乎来自sql server后台的性能是错误的,但是对于文档数据库来说可能不是那么糟糕吗?
terjetyl 2010年

3
同样从SQL Server的背景出发,我希望MongoDB一次性使用“结果集”(具有选定的返回字段)作为新查询的输入,就像SQL中的嵌套查询一样
Stijn Sanders

1
@terjetyl您必须真正计划一下。您要在前端显示哪些字段,如果单个视图中的字段数量有限,则将其作为嵌入式文档。关键是不需要进行联接。如果要进行深度分析,请在另一个数据库中进行分析。运行将数据转换为OLAP多维数据集以实现最佳性能的作业。
13年

4
从mongo 3.2版本开始,支持左连接。
Somnath Muluk

18

正如其他人指出的那样,您正在尝试从一个您根本不想做的关系数据库创建一个关系数据库,但是无论如何,如果您必须这样做,可以在这里使用它。我们首先在集合A(或您的情况下的用户)上进行foreach查找,然后将每个项目作为一个对象,然后使用object属性(在您的情况下为uid)在第二个集合(在您的情况下为注释)中查找,如果我们可以找到它,然后我们找到一个匹配项,我们可以对其进行打印或做一些事情。希望这对您有帮助,并祝您好运:)

db.users.find().forEach(
function (object) {
    var commonInBoth=db.comments.findOne({ "uid": object.uid} );
    if (commonInBoth != null) {
        printjson(commonInBoth) ;
        printjson(object) ;
    }else {
        // did not match so we don't care in this case
    }
});

这样会不会找到我们当前正在循环播放的商品?
Skarlinski'3

18

使用$ lookup$ project$ match的正确组合,您可以在多个参数上连接多个表。这是因为它们可以链接多次。

假设我们要执行以下操作(参考

SELECT S.* FROM LeftTable S
LEFT JOIN RightTable R ON S.ID =R.ID AND S.MID =R.MID WHERE R.TIM >0 AND 
S.MOB IS NOT NULL

步骤1:连结所有表格

您可以$ lookup任意数量的表。

$ lookup-查询中的每个表一个

$ unwind-因为数据正确地反规范化,否则包装在数组中

Python代码

db.LeftTable.aggregate([
                        # connect all tables

                        {"$lookup": {
                          "from": "RightTable",
                          "localField": "ID",
                          "foreignField": "ID",
                          "as": "R"
                        }},
                        {"$unwind": "R"}

                        ])

步骤2:定义所有条件

$ project:在此处定义所有条件语句,以及您要选择的所有变量。

Python代码

db.LeftTable.aggregate([
                        # connect all tables

                        {"$lookup": {
                          "from": "RightTable",
                          "localField": "ID",
                          "foreignField": "ID",
                          "as": "R"
                        }},
                        {"$unwind": "R"},

                        # define conditionals + variables

                        {"$project": {
                          "midEq": {"$eq": ["$MID", "$R.MID"]},
                          "ID": 1, "MOB": 1, "MID": 1
                        }}
                        ])

步骤3:加入所有条件

$ match-使用OR或AND等加入所有条件。可以有多个。

$ project:取消定义所有条件

Python代码

db.LeftTable.aggregate([
                        # connect all tables

                        {"$lookup": {
                          "from": "RightTable",
                          "localField": "ID",
                          "foreignField": "ID",
                          "as": "R"
                        }},
                        {"$unwind": "$R"},

                        # define conditionals + variables

                        {"$project": {
                          "midEq": {"$eq": ["$MID", "$R.MID"]},
                          "ID": 1, "MOB": 1, "MID": 1
                        }},

                        # join all conditionals

                        {"$match": {
                          "$and": [
                            {"R.TIM": {"$gt": 0}}, 
                            {"MOB": {"$exists": True}},
                            {"midEq": {"$eq": True}}
                        ]}},

                        # undefine conditionals

                        {"$project": {
                          "midEq": 0
                        }}

                        ])

表,条件和联接的几乎任何组合都可以用这种方式完成。


17

这里有一个例子“加盟” * 演员电影集合:

https://github.com/mongodb/cookbook/blob/master/content/patterns/pivot.txt

它利用.mapReduce()方法

* join-连接面向文档的数据库的替代方法


19
-1,这不是联接来自两个集合的数据。它使用来自单个集合(参与者)的数据来回绕数据。所以原来是键的东西现在是值,而值现在是键了... 与JOIN 完全不同。
埃文·特兰

12
这正是您要做的,MongoDB不是关系的,而是面向文档的。MapReduce允许以高性能处理数据(您可以使用群集等。),但是即使在简单的情况下,它也非常有用!
Thomas Decaux 2012年

14

您可以使用3.2版本提供的查找功能在Mongo中加入两个集合。在您的情况下,查询将是

db.comments.aggregate({
    $lookup:{
        from:"users",
        localField:"uid",
        foreignField:"uid",
        as:"users_comments"
    }
})

或者您也可以针对用户加入,则会有一些小的变化,如下所示。

db.users.aggregate({
    $lookup:{
        from:"comments",
        localField:"uid",
        foreignField:"uid",
        as:"users_comments"
    }
})

它的工作方式与SQL中的左右联接相同。


11

这取决于您要执行的操作。

您目前可以将其设置为规范化数据库,这很好,并且您的操作方式也很合适。

但是,还有其他方法可以做到这一点。

您可能有一个posts集合,其中包含每个帖子的注释以及对您可以迭代查询得到的用户的引用。您可以将用户名和注释一起存储,也可以将它们全部存储在一个文档中。

NoSQL的优点是它专为灵活的模式和非常快速的读写而设计。在典型的大数据场中,数据库是最大的瓶颈,与应用程序和前端服务器相比,数据库引擎更少……它们更昂贵,但功能更强大,硬盘空间也相对便宜。标准化来自于试图节省空间的概念,但是它伴随着使数据库执行复杂的联接,验证关系的完整性,执行级联操作的成本。如果开发人员正确地设计了数据库,所有这些都使开发人员免于头痛。

使用NoSQL,如果您接受冗余和存储空间不是因为它们的成本而引起的问题(执行更新所需的处理器时间和存储额外数据的硬盘驱动器成本),那么非规范化就不是问题了(对于嵌入式阵列而言成千上万的项目可能是性能问题,但大多数情况下这不是问题)。此外,您将为每个数据库集群提供多个应用程序和前端服务器。让他们承担繁重的连接工作,让数据库服务器坚持读写。

TL; DR:您在做什么很好,还有其他方法可以做到。查看mongodb文档的数据模型模式以获取一些出色的示例。http://docs.mongodb.org/manual/data-modeling/


8
我对此表示质疑:“规范化来自试图节省空间的概念”。恕我直言归一化来自避免冗余的概念。假设您将用户名和博客文章一起存储。如果她结婚怎么办?在未规范化的模型中,您将不得不遍历所有帖子并更改名称。在规范化模型中,您通常会更改一个记录。
DanielKhan 2013年

@DanielKhan防止冗余和节省空间是相似的概念,但是我确实同意,在重新分析中,冗余是此设计的根本原因。我会改写。感谢您的来信。
2013年

11

有很多驱动程序支持的规范,称为DBRef。

DBRef是用于在文档之间创建引用的更正式的规范。DBRef(通常)包括集合名称和对象ID。如果集合可以从一个文档更改为下一个文档,则大多数开发人员仅使用DBRef。如果您引用的收藏集始终是相同的,则上面概述的手动引用会更有效。

取自MongoDB文档:数据模型>数据模型参考> 数据库参考


11

$ lookup(汇总)

对同一数据库中的未分片集合执行左外部联接,以从“联接”集合中过滤文档以进行处理。在每个输入文档中,$ lookup阶段都会添加一个新的数组字段,其元素是“ joined”集合中的匹配文档。$ lookup阶段将这些经过重整的文档传递到下一个阶段。$ lookup阶段具有以下语法:

平等比赛

为了在输入文档的字段与“ joined”集合的文档的字段之间执行相等匹配,$ lookup阶段具有以下语法:

{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

该操作将对应于以下伪SQL语句:

SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (SELECT <documents as determined from the pipeline>
                               FROM <collection to join>
                               WHERE <pipeline> );

Mongo URL


子查询与join完全不同,如果左侧表很大,则子查询意味着每一行都必须自己进行查询。它将变得非常缓慢。联接在sql中非常快。
yww325

8

3.2.6之前,Mongodb不像mysql一样支持连接查询。下面的解决方案为您工作。

 db.getCollection('comments').aggregate([
        {$match : {pid : 444}},
        {$lookup: {from: "users",localField: "uid",foreignField: "uid",as: "userData"}},
   ])


3

MongoDB不允许加入,但是您可以使用插件来处理。检查mongo-join插件。这是最好的,我已经用过了。您可以像这样直接使用npm安装它npm install mongo-join。您可以通过示例查看完整的文档

(++)当我们需要加入(N)个集合时非常有用的工具

(-)我们可以在查询的顶层应用条件

var Join = require('mongo-join').Join, mongodb = require('mongodb'), Db = mongodb.Db, Server = mongodb.Server;
db.open(function (err, Database) {
    Database.collection('Appoint', function (err, Appoints) {

        /* we can put conditions just on the top level */
        Appoints.find({_id_Doctor: id_doctor ,full_date :{ $gte: start_date },
            full_date :{ $lte: end_date }}, function (err, cursor) {
            var join = new Join(Database).on({
                field: '_id_Doctor', // <- field in Appoints document
                to: '_id',         // <- field in User doc. treated as ObjectID automatically.
                from: 'User'  // <- collection name for User doc
            }).on({
                field: '_id_Patient', // <- field in Appoints doc
                to: '_id',         // <- field in User doc. treated as ObjectID automatically.
                from: 'User'  // <- collection name for User doc
            })
            join.toArray(cursor, function (err, joinedDocs) {

                /* do what ever you want here */
                /* you can fetch the table and apply your own conditions */
                .....
                .....
                .....


                resp.status(200);
                resp.json({
                    "status": 200,
                    "message": "success",
                    "Appoints_Range": joinedDocs,


                });
                return resp;


            });

    });

2

您可以使用聚合管道来做到这一点,但是自己编写却很痛苦。

您可以用来mongo-join-query根据查询自动创建聚合管道。

这是查询的样子:

const mongoose = require("mongoose");
const joinQuery = require("mongo-join-query");

joinQuery(
    mongoose.models.Comment,
    {
        find: { pid:444 },
        populate: ["uid"]
    },
    (err, res) => (err ? console.log("Error:", err) : console.log("Success:", res.results))
);

您的结果将在该uid字段中包含用户对象,并且您可以根据需要链接多个级别。您可以填充对用户的引用,该用户对团队的引用,对其他事物的引用等。

免责声明:我写信mongo-join-query来解决这个确切的问题。



-2

不,看来您做错了。MongoDB联接是“客户端”。就像您说的那样:

此刻,我首先要获取符合我的条件的注释,然后找出该结果集中的所有uid,获取用户对象,并将它们与注释结果合并。好像我做错了。

1) Select from the collection you're interested in.
2) From that collection pull out ID's you need
3) Select from other collections
4) Decorate your original results.

这不是“真正的”联接,但是实际上它比SQL联接要有用得多,因为您不必为“许多”侧联接进行重复的行处理,而只需修饰原始选择的联接即可。

此页面上有很多废话和FUD。事实证明,五年后,MongoDB仍然是一回事。


“您不必为“许多”双面联接处理重复的行” -不知道您的意思是什么。你能澄清一下吗?
Mark Amery

1
@MarkAmery,当然。在SQL中,nn关系将返回重复的行。例如朋友。如果Bob是Mary和Jane的朋友,那么Bob会得到2行:Bob,Mary和Bob,Jane。2 Bobs是一个谎言,只有一个Bob。通过客户端加入,您可以从Bob开始,并装饰自己喜欢的方式:Bob,“ Mary and Jane”。SQL让您使用子查询来执行此操作,但这是在db服务器上完成的工作,而该工作可以在客户端上完成。
Michael Cole 2015年

-3

我认为,如果您需要标准化的数据表-您需要尝试其他一些数据库解决方案。

但是我在Git上找到了MOngo 的解决方案。顺便说一句,在插入代码中-它具有电影的名称,但具有noi电影的ID

问题

您有一个演员集合,其中包含他们完成的电影。

您要生成一个电影集合,每个电影集合中都有一组Actor。

一些样本数据

 db.actors.insert( { actor: "Richard Gere", movies: ['Pretty Woman', 'Runaway Bride', 'Chicago'] });
 db.actors.insert( { actor: "Julia Roberts", movies: ['Pretty Woman', 'Runaway Bride', 'Erin Brockovich'] });

我们需要遍历Actor文档中的每个电影并分别发出每个电影。

这里的捕获处于还原阶段。我们无法从reduce阶段发出数组,因此必须在返回的“ value”文档内部构建Actors数组。

编码
map = function() {
  for(var i in this.movies){
    key = { movie: this.movies[i] };
    value = { actors: [ this.actor ] };
    emit(key, value);
  }
}

reduce = function(key, values) {
  actor_list = { actors: [] };
  for(var i in values) {
    actor_list.actors = values[i].actors.concat(actor_list.actors);
  }
  return actor_list;
}

注意actor_list实际上是一个包含数组的javascript对象。还要注意,映射发出相同的结构。

运行以下命令以执行map / reduce,将其输出到“ pivot”集合并打印结果:

printjson(db.actors.mapReduce(map,reduce,“ pivot”)); db.pivot.find()。forEach(printjson);

这是示例输出,请注意,“漂亮女人”和“逃亡新娘”都具有“理查德·基尔”和“朱莉亚·罗伯茨”。

{ "_id" : { "movie" : "Chicago" }, "value" : { "actors" : [ "Richard Gere" ] } }
{ "_id" : { "movie" : "Erin Brockovich" }, "value" : { "actors" : [ "Julia Roberts" ] } }
{ "_id" : { "movie" : "Pretty Woman" }, "value" : { "actors" : [ "Richard Gere", "Julia Roberts" ] } }
{ "_id" : { "movie" : "Runaway Bride" }, "value" : { "actors" : [ "Richard Gere", "Julia Roberts" ] } }


请注意,此答案的大部分内容(即可理解的英语内容)是从答案提供的GitHub链接上的MongoDB Cookbook复制而来的。
Mark Amery 2015年

-4

我们可以使用mongoDB子查询合并两个集合。这是示例,评论-

`db.commentss.insert([
  { uid:12345, pid:444, comment:"blah" },
  { uid:12345, pid:888, comment:"asdf" },
  { uid:99999, pid:444, comment:"qwer" }])`

用户-

db.userss.insert([
  { uid:12345, name:"john" },
  { uid:99999, name:"mia"  }])

MongoDB子查询JOIN--

`db.commentss.find().forEach(
    function (newComments) {
        newComments.userss = db.userss.find( { "uid": newComments.uid } ).toArray();
        db.newCommentUsers.insert(newComments);
    }
);`

从新生成的集合中获取结果-

db.newCommentUsers.find().pretty()

结果 -

`{
    "_id" : ObjectId("5511236e29709afa03f226ef"),
    "uid" : 12345,
    "pid" : 444,
    "comment" : "blah",
    "userss" : [
        {
            "_id" : ObjectId("5511238129709afa03f226f2"),
            "uid" : 12345,
            "name" : "john"
        }
    ]
}
{
    "_id" : ObjectId("5511236e29709afa03f226f0"),
    "uid" : 12345,
    "pid" : 888,
    "comment" : "asdf",
    "userss" : [
        {
            "_id" : ObjectId("5511238129709afa03f226f2"),
            "uid" : 12345,
            "name" : "john"
        }
    ]
}
{
    "_id" : ObjectId("5511236e29709afa03f226f1"),
    "uid" : 99999,
    "pid" : 444,
    "comment" : "qwer",
    "userss" : [
        {
            "_id" : ObjectId("5511238129709afa03f226f3"),
            "uid" : 99999,
            "name" : "mia"
        }
    ]
}`

希望如此会有所帮助。


7
您为什么基本上复制了这个几乎相同的一年前的答案? stackoverflow.com/a/22739813/4186945
Hackel
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.