Запрос MongoDB для получения списка документов с количеством внешних связанных документов

У меня есть база данных 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"
    // ...
}

У пользователя много книг, а у книги много страниц. Причина, по которой каждый объект является отдельным документом, заключается в том, что у пользователя может быть тысячи книг, а книга может иметь тысячи страниц, поэтому, если бы все было в одном документе, мы могли бы довольно легко достичь предела в 16 МБ.

Каков наилучший способ получить список книг для указанного userId с pageCount поле для каждой книги?

Это результат JSON, который мне нужен.

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

С базой данных SQL это было бы довольно просто с количеством соединений, но с mongodb я не вижу никакого простого решения, кроме как делать отдельные запросы, чтобы получить список книг и затем получить количество страниц для каждой книги.

3 ответа

Он не дает прямого ответа на вопрос, а дает некоторые идеи относительно

делать отдельные запросы, чтобы получить список книг, а затем получить количество страниц для каждой книги

часть. Это не всегда плохо. Mongodb достаточно эффективен в простых запросах, поэтому я дам вам несколько цифр, чтобы рассмотреть производительность одного конвейера $ lookup по сравнению с несколькими запросами, и призываю вас протестировать типичные запросы в вашем наборе данных. Egpagination может иметь огромное значение, если вам не нужны все данные сразу.

настроить

Небольшая база данных на 100 пользователей X 1000 книг X 1000 страниц каждая на крошечном 1 ВЦП / 2 ГБ памяти / 50 ГБ Диск / LON1 - Ubuntu MongoDB 3.4.10 на 16.04 дроплет.

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. результаты слияния

    Не запрос, но должно быть сделано на уровне приложения. Javascript mongoshell занимает несколько миллисекунд, что дает общее время на получение всех данных менее чем за 2 секунды.

С платформой агрегации 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" }
                }
            }
        }
    }}
])

Вы можете использовать $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

Другие вопросы по тегам