0

我有一个 mongodb 数据库,其中包含大约如下的集合文档:

// user document
{
    _id: $oid,
    name: "name",
    description: "description".
    // ...
}

// book document
{
    _id: $oid,
    userId: "..."
    name: "name",
    description: "description"
    // ...
}

// page document
{
    _id: $oid,
    bookId: "..."
    name: "name",
    description: "description"
    // ...
}

一个用户有很多书,而一本书有很多页。每个实体都是一个单独的文档的原因是因为用户可以拥有数千本书,而一本书可以有数千页,所以,如果所有内容都在一个文档中,我们可以很容易地达到 16MB 的限制。

userId检索每本书指定pageCount字段的书籍列表的最佳方法是什么?

这是我需要的 json 结果。

{
    books: [{
        _id: $oid,
        name: "name1",
        description: "description1",
        pageCount: 8
    }, {
        _id: $oid,
        name: "name2",
        description: "description2",
        pageCount: 12
    },
        // ...
    ]
}

使用连接计数非常简单的 SQL 数据库,但是使用 mongodb 除了进行单独的查询以获取书籍列表然后获取每本书的页数之外,我看不到任何简单的解决方案。

4

3 回答 3

2

它没有直接回答这个问题,而是给出了一些关于

进行单独查询以获取书籍列表,然后获取每本书的页数

部分。这并不总是一件坏事。Mongodb 在简单查询中非常有效,因此我给您一些数字来考虑单个 $lookup 管道与多个查询的性能,并鼓励您在数据集上测试典型查询。如果您不需要一次获取所有数据,则电子分页可以产生巨大的影响。

设置

一个包含 100 个用户 X 1,000 本书 X 1,000 页的小型数据库,每个用户在一个微型 1 vCPU / 2 GB 内存 / 50 GB 磁盘 / LON1 - Ubuntu MongoDB 3.4.10 on 16.04 droplet。

pages集合创建如下:

for USERID in {1..100}; do   
    echo "" > pages.json;     
    for BOOKID in {1..1000}; do       
       ./node_modules/.bin/mgeneratejs "{\"bookId\": \"$USERID-$BOOKID\", \"name\": {\"\$sentence\":{\"words\":3}}, \"description\": \"\$paragraph\"}" -n 1000 >> pages.json
    done     
    cat pages.json | mongoimport -d so -c pages 
done

一个books几乎是一样的。

基本数据:

db.books.stats(1024*1024)
    "ns" : "so.books",
    "size" : 50,
    "count" : 100000,
    "avgObjSize" : 533,
    "storageSize" : 52,
    "nindexes" : 2,
    "totalIndexSize" : 1,
    "indexSizes" : {
            "_id_" : 0,
            "userId_1" : 0
    },

db.pages.stats(1024*1024)
    "ns" : "so.pages",
    "size" : 51673,
    "count" : 100000000,
    "avgObjSize" : 541,
    "storageSize" : 28920,
    "nindexes" : 2,
    "totalIndexSize" : 1424,
    "indexSizes" : {
            "_id_" : 994,
            "bookId_1" : 430
    },

$查找

@chridam 回答的管道

db.books.aggregate([
    { "$match": { "userId": 18 } },
    { "$lookup": {
        "from": "pages",
        "localField": "_id",
        "foreignField": "bookId",
        "as": "pageCount"
    }},
    { "$addFields": {
        "pageCount": { "$size": "$pageCount" }
    }}
]) 

提供极快的响应:

    "op" : "command",
    "command" : {
            "aggregate" : "books"
    },
    "keysExamined" : 1000,
    "docsExamined" : 1000,
    "nreturned" : 101,
    "responseLength" : 57234,
    "millis" : 1028

对于前 100 个文档,让您在一秒钟内开始处理文档。

整个事情的总时间:

db.books.aggregate([
    { "$match": { "userId": 18 } },
    { "$lookup": {
        "from": "pages",
        "localField": "_id",
        "foreignField": "bookId",
        "as": "pageCount"
    }},
    { "$addFields": {
        "pageCount": { "$size": "$pageCount" }
    }}
]).toArray()

再增加 8 秒:

    "op" : "getmore",
    "query" : {
            "getMore" : NumberLong("32322423895"),
            "collection" : "books"
    },
    "keysExamined" : 0,
    "docsExamined" : 0,
    "nreturned" : 899,
    "responseLength" : 500060,
    "millis" : 8471

检索所有数据的总时间超过 9 秒

多个查询

  1. 检索书籍:

    let bookIds = []; 
    db.books.find({userId:12}).forEach(b=>{bookIds.push(b._id);});
    

    在 10 毫秒内填充数组:

    "op" : "query",
    "query" : {
            "find" : "books",
            "filter" : {
                    "userId" : 34
            }
    },
    "keysExamined" : 101,
    "docsExamined" : 101,
    "nreturned" : 101,
    "responseLength" : 54710,
    "millis" : 3
    

    "op" : "getmore",
    "query" : {
            "getMore" : NumberLong("34224552674"),
            "collection" : "books"
    },
    "keysExamined" : 899,
    "docsExamined" : 899,
    "nreturned" : 899,
    "responseLength" : 485698,
    "millis" : 7
    
  2. 计算页数:

    db.pages.aggregate([
        { $match: { bookId: { $in: bookIds } } }, 
        { $group: { _id: "$bookId", cnt: { $sum: 1 } } }
    ]).toArray()
    

    总共需要 1.5 秒:

    "op" : "command",
    "command" : {
            "aggregate" : "pages"
    },
    "keysExamined" : 1000001,
    "docsExamined" : 0,
    "nreturned" : 101,
    "responseLength" : 3899,
    "millis" : 1574
    

    "op" : "getmore",
    "query" : {
            "getMore" : NumberLong("58311204806"),
            "collection" : "pages"
    },
    "keysExamined" : 0,
    "docsExamined" : 0,
    "nreturned" : 899,
    "responseLength" : 34935,
    "millis" : 0
    
  3. 合并结果

    不是查询,但应该在应用程序级别完成。在 mongoshell javascript 中需要几毫秒,这使得检索所有数据的总时间少于 2 秒

于 2018-03-26T13:01:29.767 回答
1

使用 MongoDB 的聚合框架,有一个称为管道阶段$lookup,它允许您对同一数据库中的另一个集合进行左外连接,以过滤来自“已连接”集合的文档以进行处理。

因此,有了这个武器,您可以运行聚合管道操作,将书籍集合连接到页面集合。

在管道步骤中,您可以pageCount通过从“连接”查询结果数组的大小来获得。

假设您的 MongoDB 服务器版本至少为 3.4,请考虑运行以下聚合操作以获得所需的结果:

db.books.aggregate([
    { "$match": { "userId": userId } },
    { "$lookup": {
        "from": "pages",
        "localField": "_id",
        "foreignField": "bookId",
        "as": "pageCount"
    }},
    { "$addFields": {
        "pageCount": { "$size": "$pageCount" }
    }}
])

或者,您可以将集合中的$lookup管道运行为users

db.user.aggregate([
    { "$match": { "_id": userId } },
    { "$lookup": {
        "from": "books",
        "localField": "_id",
        "foreignField": "userId",
        "as": "books"
    }},
    { "$lookup": {
        "from": "pages",
        "localField": "books._id",
        "foreignField": "bookId",
        "as": "pages"
    }},
    { "$addFields": {
        "books": {
            "$map": {
                "input": "$books",
                "as": "book",
                "in": {
                    "name": "$$book.name",
                    "description": "$$book.description",
                    "pageCount": { "$size": "$$book.pages" }
                }
            }
        }
    }}
])
于 2018-03-23T10:20:51.040 回答
0

您可以使用$lookup聚合框架中的阶段:

db.Users.aggregate([
    {$match: {_id: userId}},
    {$lookup: {
        from: "Book",
        localField: "userId",
        foreignField: "_id",
        as: "book"
    }},
    {$lookup: {
        from: "Page",
        localField: "bookId",
        foreignField: "book._id",
        as: "page"
    }}
])

并添加阶段$group以计算页数。但我认为这个查询会很慢。如果你想在之后对你的集合进行分片,或者如果它已经是这种情况,你就不能使用 $lookup

于 2018-03-23T10:19:15.337 回答