我如何在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,获取用户对象,并将其与评论的结果。好像我做错了。

评论

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

正确的,$ lookup是在MongoDB 3.2中引入的。可以在docs.mongodb.org/master/reference/operator/aggregation/lookup/…中找到详细信息。...

#1 楼

从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不是关系数据库,并且开发人员会谨慎推荐特定的用例for $ lookup,但至少从3.2版本开始,现在可以使用MongoDB进行联接。

评论


@clayton:那么两个以上的集合呢?

– Dipen Dedania
16年1月8日在10:36

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

– Clayton Gulick
16年1月9日在1:01

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

–Prateek Singh
16年1月1日在16:52

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

–user3413723
16年6月21日在4:07

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

– gtiwari333
16年7月18日在12:55

#2 楼

mongodb官方网站上的此页面完全解决了这个问题:
https://mongodb-documentation.readthedocs.io/en/latest/ecosystem/tutorial/model-data-for-ruby-on-rails.html

显示故事列表时,我们需要显示发布故事的用户名。如果使用关系数据库,则可以对用户和商店执行联接,并在单个查询中获得所有对象。但是MongoDB不支持联接,因此有时需要一些非规范化。在这里,这意味着要缓存“用户名”属性。
关系纯粹主义者可能已经感到不安,好像我们在违反某些普遍法律一样。但是请记住,MongoDB集合不等同于关系表;每个都有一个独特的设计目标。标准化表提供了原子的,隔离的数据块。但是,文档更紧密地代表了整个对象。就社交新闻网站而言,可以说用户名是所发布故事的固有内容。


评论


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

–烧雪
13年11月4日在1:53

@dudelgrincen我也应该说不是每个项目或设计都适用。如果您在SQL类型数据库中具有某些功能,为什么要更改它?如果您无法通过schema来使用noSQL,那就不要。

–烧雪
13年11月12日,0:30

在NoSQL系统上,迁移和不断发展的模式也更容易管理。

–贾斯汀
2014年5月6日20:09

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

–伊沃·佩雷拉(Ivo Pereira)
16 Mar 2 '16 at 17:39

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

– Omid
17年11月30日在20:43

#3 楼

我们可以使用mongodb客户端控制台在几行中通过简单的功能将一个集合中的所有数据合并/合并,现在我们可以执行所需的查询。
在一个完整的示例下面,
< br ..-作者:

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()


希望这条线能对您有所帮助。

评论


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

– Abbood
2014年5月30日13:46

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

– Balupton
2014年6月4日下午5:55

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

– Sandeep Giri
2014年6月19日15:31



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

– Peter Krauss
2015年9月22日14:13在

@SandeepGiri我如何做聚合管道,因为我在分离的集合中确实有非常密集的数据需要加入?

– Yasine Abdul-Rahman
2015年10月7日在20:16

#4 楼

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

评论


似乎来自sql server后台的性能是错误的,但是对于文档db来说可能并不那么糟糕?

–terjetyl
2010年7月15日在18:20

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

– Stijn Sanders
2010-11-26 23:17

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

–烧雪
13年4月4日在1:56

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

– Somnath Muluk
15年11月26日在11:12

#5 楼

正如其他人指出的那样,您正在尝试从一个您根本不想做的关系数据库创建一个关系数据库,但是无论如何,如果您必须这样做,可以在这里使用它。我们首先在集合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
17 Mar 29 '17 at 12:58

#6 楼

使用$ 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
                        }}

                        ])


几乎所有的表组合,条件和连接可以通过这种方式完成。

#7 楼

以下是“加入” *演员和电影收藏集的示例:

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

它使用.mapReduce()方法

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

评论


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

–埃文·特兰(Evan Teran)
2012年5月22日17:44

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

– Thomas Decaux
2012年6月17日19:16



#8 楼

您可以使用3.2版本提供的查找功能在Mongo中加入两个集合。在您的情况下,查询为/>
db.comments.aggregate({
    $lookup:{
        from:"users",
        localField:"uid",
        foreignField:"uid",
        as:"users_comments"
    }
})


它将与SQL中的左右联接相同。

#9 楼

$ 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
4月30日下午2:14

#10 楼

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

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

但是,还有其他方法可以实现。

您可以拥有一个posts集合,其中对每个帖子都嵌入了注释,并带有对用户的引用,您可以迭代查询得到。您可以将用户名和注释一起存储,也可以将它们全部存储在一个文档中。

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

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

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

评论


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

–DanielKhan
13年11月27日在13:28



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

–烧雪
13年11月27日在19:15

#11 楼

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


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


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

#12 楼

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

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


#13 楼

您可以运行SQL查询,包括使用Postgres的mongo_fdw在MongoDB上进行联接。

#14 楼

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

(++)当我们需要加入(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;


            });

    });


#15 楼

您可以使用聚合管道来完成此操作,但是您自己编写它很麻烦。

您可以使用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来解决这个确切的问题。

#16 楼

playORM可以使用S-SQL(可伸缩SQL)为您做到这一点,它仅添加分区即可在分区内进行联接。

#17 楼

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


目前,我首先要获取符合我的条件的注释,然后找出该结果集中的所有uid,并获取用户对象,然后将其与评论结果合并。似乎我做错了。对于“许多”侧连接,不必处理重复的行,而只需装饰原始选择的集即可。

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

评论


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

–马克·阿默里(Mark Amery)
2015年9月20日在20:23



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

–迈克尔·科尔(Michael Cole)
2015年9月21日14:51



#18 楼

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

但是我发现在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阶段发出数组,因此必须在返回的“值”文档中构建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发出相同的结构。

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

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

这是示例输出,请注意,“ 《俏皮女人》和《逃亡新娘》都有《理查德·基尔》和《朱莉娅·罗伯茨》。


评论


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

–马克·阿默里(Mark Amery)
2015年10月5日14:23

#19 楼

我们可以使用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 sub查询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"
        }
    ]
}`


希望如此,这会有所帮助。

评论


您为什么基本上复制了这个几乎相同的一年前的答案? stackoverflow.com/a/22739813/4186945

– Hackel
2015年5月5日20:45