các giá trị nhóm mongodb theo nhiều trường


106

Ví dụ, tôi có các tài liệu sau:

{
  "addr": "address1",
  "book": "book1"
},
{
  "addr": "address2",
  "book": "book1"
},
{
  "addr": "address1",
  "book": "book5"
},
{
  "addr": "address3",
  "book": "book9"
},
{
  "addr": "address2",
  "book": "book5"
},
{
  "addr": "address2",
  "book": "book1"
},
{
  "addr": "address1",
  "book": "book1"
},
{
  "addr": "address15",
  "book": "book1"
},
{
  "addr": "address9",
  "book": "book99"
},
{
  "addr": "address90",
  "book": "book33"
},
{
  "addr": "address4",
  "book": "book3"
},
{
  "addr": "address5",
  "book": "book1"
},
{
  "addr": "address77",
  "book": "book11"
},
{
  "addr": "address1",
  "book": "book1"
}

và như thế.


Làm cách nào để tôi có thể đưa ra một yêu cầu, trong đó sẽ mô tả N địa chỉ hàng đầu và M cuốn sách hàng đầu trên mỗi địa chỉ?

Ví dụ về kết quả mong đợi:

address1 | cuốn_1: 5
| cuốn_2: 10
| sách_3: 50
| tổng số: 65
______________________
address2 | cuốn_1: 10
| cuốn_2: 10
| ...
| book_M: 10
| tổng: M * 10
...
______________________
addressN | cuốn_1: 20
| cuốn_2: 20
| ...
| sách_M: 20
| tổng: M * 20

Câu trả lời:


202

Tóm tắt TLDR

Trong các bản phát hành MongoDB hiện đại, bạn có thể thực hiện điều này $slicechỉ với kết quả tổng hợp cơ bản. Đối với các kết quả "lớn", thay vào đó hãy chạy các truy vấn song song cho mỗi nhóm (danh sách minh họa ở cuối câu trả lời) hoặc đợi SERVER-9377 giải quyết, điều này sẽ cho phép "giới hạn" số lượng mục đối $pushvới mảng.

db.books.aggregate([
    { "$group": {
        "_id": {
            "addr": "$addr",
            "book": "$book"
        },
        "bookCount": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.addr",
        "books": { 
            "$push": { 
                "book": "$_id.book",
                "count": "$bookCount"
            },
        },
        "count": { "$sum": "$bookCount" }
    }},
    { "$sort": { "count": -1 } },
    { "$limit": 2 },
    { "$project": {
        "books": { "$slice": [ "$books", 2 ] },
        "count": 1
    }}
])

Bản xem trước MongoDB 3.6

Vẫn không giải quyết được SERVER-9377 , nhưng trong bản phát hành này $lookupcho phép một tùy chọn "không tương quan" mới lấy một "pipeline"biểu thức làm đối số thay vì "localFields""foreignFields"tùy chọn. Sau đó, điều này cho phép "tự tham gia" với một biểu thức đường ống khác, trong đó chúng ta có thể áp dụng $limitđể trả về kết quả "top-n".

db.books.aggregate([
  { "$group": {
    "_id": "$addr",
    "count": { "$sum": 1 }
  }},
  { "$sort": { "count": -1 } },
  { "$limit": 2 },
  { "$lookup": {
    "from": "books",
    "let": {
      "addr": "$_id"
    },
    "pipeline": [
      { "$match": { 
        "$expr": { "$eq": [ "$addr", "$$addr"] }
      }},
      { "$group": {
        "_id": "$book",
        "count": { "$sum": 1 }
      }},
      { "$sort": { "count": -1  } },
      { "$limit": 2 }
    ],
    "as": "books"
  }}
])

Sự bổ sung khác ở đây tất nhiên là khả năng nội suy biến thông qua $exprviệc sử dụng $matchđể chọn các mục phù hợp trong "kết hợp", nhưng tiền đề chung là "đường dẫn trong đường ống" nơi nội dung bên trong có thể được lọc bằng các kết quả phù hợp từ nguồn gốc. . Vì bản thân chúng đều là "đường ống", chúng ta có thể $limitcho mỗi kết quả một cách riêng biệt.

Đây sẽ là lựa chọn tốt nhất tiếp theo để chạy các truy vấn song song và thực sự sẽ tốt hơn nếu $matchđược phép và có thể sử dụng một chỉ mục trong quá trình xử lý "đường ống con". Vì vậy, không sử dụng "giới hạn đối với $push" như vấn đề được tham chiếu yêu cầu, nó thực sự mang lại thứ gì đó sẽ hoạt động tốt hơn.


Nội dung gốc

Có vẻ như bạn đã vấp phải vấn đề "N" hàng đầu. Theo một cách nào đó, vấn đề của bạn khá dễ giải quyết mặc dù không có giới hạn chính xác mà bạn yêu cầu:

db.books.aggregate([
    { "$group": {
        "_id": {
            "addr": "$addr",
            "book": "$book"
        },
        "bookCount": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.addr",
        "books": { 
            "$push": { 
                "book": "$_id.book",
                "count": "$bookCount"
            },
        },
        "count": { "$sum": "$bookCount" }
    }},
    { "$sort": { "count": -1 } },
    { "$limit": 2 }
])

Bây giờ sẽ cho bạn một kết quả như sau:

{
    "result" : [
            {
                    "_id" : "address1",
                    "books" : [
                            {
                                    "book" : "book4",
                                    "count" : 1
                            },
                            {
                                    "book" : "book5",
                                    "count" : 1
                            },
                            {
                                    "book" : "book1",
                                    "count" : 3
                            }
                    ],
                    "count" : 5
            },
            {
                    "_id" : "address2",
                    "books" : [
                            {
                                    "book" : "book5",
                                    "count" : 1
                            },
                            {
                                    "book" : "book1",
                                    "count" : 2
                            }
                    ],
                    "count" : 3
            }
    ],
    "ok" : 1
}

Vì vậy, điều này khác với những gì bạn đang yêu cầu, trong khi chúng tôi nhận được kết quả hàng đầu cho các giá trị địa chỉ, lựa chọn "sách" cơ bản không chỉ giới hạn ở một lượng kết quả bắt buộc.

Điều này hóa ra là rất khó thực hiện, nhưng nó có thể được thực hiện mặc dù độ phức tạp chỉ tăng lên với số lượng mục bạn cần kết hợp. Để đơn giản, chúng tôi có thể giữ tối đa 2 trận đấu:

db.books.aggregate([
    { "$group": {
        "_id": {
            "addr": "$addr",
            "book": "$book"
        },
        "bookCount": { "$sum": 1 }
    }},
    { "$group": {
        "_id": "$_id.addr",
        "books": { 
            "$push": { 
                "book": "$_id.book",
                "count": "$bookCount"
            },
        },
        "count": { "$sum": "$bookCount" }
    }},
    { "$sort": { "count": -1 } },
    { "$limit": 2 },
    { "$unwind": "$books" },
    { "$sort": { "count": 1, "books.count": -1 } },
    { "$group": {
        "_id": "$_id",
        "books": { "$push": "$books" },
        "count": { "$first": "$count" }
    }},
    { "$project": {
        "_id": {
            "_id": "$_id",
            "books": "$books",
            "count": "$count"
        },
        "newBooks": "$books"
    }},
    { "$unwind": "$newBooks" },
    { "$group": {
      "_id": "$_id",
      "num1": { "$first": "$newBooks" }
    }},
    { "$project": {
        "_id": "$_id",
        "newBooks": "$_id.books",
        "num1": 1
    }},
    { "$unwind": "$newBooks" },
    { "$project": {
        "_id": "$_id",
        "num1": 1,
        "newBooks": 1,
        "seen": { "$eq": [
            "$num1",
            "$newBooks"
        ]}
    }},
    { "$match": { "seen": false } },
    { "$group":{
        "_id": "$_id._id",
        "num1": { "$first": "$num1" },
        "num2": { "$first": "$newBooks" },
        "count": { "$first": "$_id.count" }
    }},
    { "$project": {
        "num1": 1,
        "num2": 1,
        "count": 1,
        "type": { "$cond": [ 1, [true,false],0 ] }
    }},
    { "$unwind": "$type" },
    { "$project": {
        "books": { "$cond": [
            "$type",
            "$num1",
            "$num2"
        ]},
        "count": 1
    }},
    { "$group": {
        "_id": "$_id",
        "count": { "$first": "$count" },
        "books": { "$push": "$books" }
    }},
    { "$sort": { "count": -1 } }
])

Vì vậy, điều đó thực sự sẽ cung cấp cho bạn 2 "cuốn sách" hàng đầu từ hai mục "địa chỉ" hàng đầu.

Nhưng đối với tiền của tôi, hãy ở lại với biểu mẫu đầu tiên và sau đó chỉ cần "cắt" các phần tử của mảng được trả về để lấy các phần tử "N" đầu tiên.


Mã trình diễn

Mã trình diễn thích hợp để sử dụng với các phiên bản LTS hiện tại của NodeJS từ các bản phát hành v8.x và v10.x. Đó là chủ yếu cho async/awaitcú pháp, nhưng không có gì thực sự trong quy trình chung có bất kỳ hạn chế nào như vậy và điều chỉnh với một chút thay đổi đối với các lời hứa đơn giản hoặc thậm chí quay trở lại triển khai gọi lại đơn giản.

index.js

const { MongoClient } = require('mongodb');
const fs = require('mz/fs');

const uri = 'mongodb://localhost:27017';

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {
    const client = await MongoClient.connect(uri);

    const db = client.db('bookDemo');
    const books = db.collection('books');

    let { version } = await db.command({ buildInfo: 1 });
    version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);

    // Clear and load books
    await books.deleteMany({});

    await books.insertMany(
      (await fs.readFile('books.json'))
        .toString()
        .replace(/\n$/,"")
        .split("\n")
        .map(JSON.parse)
    );

    if ( version >= 3.6 ) {

    // Non-correlated pipeline with limits
      let result = await books.aggregate([
        { "$group": {
          "_id": "$addr",
          "count": { "$sum": 1 }
        }},
        { "$sort": { "count": -1 } },
        { "$limit": 2 },
        { "$lookup": {
          "from": "books",
          "as": "books",
          "let": { "addr": "$_id" },
          "pipeline": [
            { "$match": {
              "$expr": { "$eq": [ "$addr", "$$addr" ] }
            }},
            { "$group": {
              "_id": "$book",
              "count": { "$sum": 1 },
            }},
            { "$sort": { "count": -1 } },
            { "$limit": 2 }
          ]
        }}
      ]).toArray();

      log({ result });
    }

    // Serial result procesing with parallel fetch

    // First get top addr items
    let topaddr = await books.aggregate([
      { "$group": {
        "_id": "$addr",
        "count": { "$sum": 1 }
      }},
      { "$sort": { "count": -1 } },
      { "$limit": 2 }
    ]).toArray();

    // Run parallel top books for each addr
    let topbooks = await Promise.all(
      topaddr.map(({ _id: addr }) =>
        books.aggregate([
          { "$match": { addr } },
          { "$group": {
            "_id": "$book",
            "count": { "$sum": 1 }
          }},
          { "$sort": { "count": -1 } },
          { "$limit": 2 }
        ]).toArray()
      )
    );

    // Merge output
    topaddr = topaddr.map((d,i) => ({ ...d, books: topbooks[i] }));
    log({ topaddr });

    client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

books.json

{ "addr": "address1",  "book": "book1"  }
{ "addr": "address2",  "book": "book1"  }
{ "addr": "address1",  "book": "book5"  }
{ "addr": "address3",  "book": "book9"  }
{ "addr": "address2",  "book": "book5"  }
{ "addr": "address2",  "book": "book1"  }
{ "addr": "address1",  "book": "book1"  }
{ "addr": "address15", "book": "book1"  }
{ "addr": "address9",  "book": "book99" }
{ "addr": "address90", "book": "book33" }
{ "addr": "address4",  "book": "book3"  }
{ "addr": "address5",  "book": "book1"  }
{ "addr": "address77", "book": "book11" }
{ "addr": "address1",  "book": "book1"  }

45

Sử dụng hàm tổng hợp như dưới đây:

[
{$group: {_id : {book : '$book',address:'$addr'}, total:{$sum :1}}},
{$project : {book : '$_id.book', address : '$_id.address', total : '$total', _id : 0}}
]

nó sẽ cho bạn kết quả như sau:

        {
            "total" : 1,
            "book" : "book33",
            "address" : "address90"
        }, 
        {
            "total" : 1,
            "book" : "book5",
            "address" : "address1"
        }, 
        {
            "total" : 1,
            "book" : "book99",
            "address" : "address9"
        }, 
        {
            "total" : 1,
            "book" : "book1",
            "address" : "address5"
        }, 
        {
            "total" : 1,
            "book" : "book5",
            "address" : "address2"
        }, 
        {
            "total" : 1,
            "book" : "book3",
            "address" : "address4"
        }, 
        {
            "total" : 1,
            "book" : "book11",
            "address" : "address77"
        }, 
        {
            "total" : 1,
            "book" : "book9",
            "address" : "address3"
        }, 
        {
            "total" : 1,
            "book" : "book1",
            "address" : "address15"
        }, 
        {
            "total" : 2,
            "book" : "book1",
            "address" : "address2"
        }, 
        {
            "total" : 3,
            "book" : "book1",
            "address" : "address1"
        }

Tôi không nhận được định dạng kết quả mong đợi của bạn, vì vậy hãy sửa đổi định dạng này thành định dạng bạn cần.


1
Điều đó chỉ giải quyết một phần của vấn đề, và không làm "đầu" cho hai nhóm.
WiredPrairie

Hơn nữa về nhận xét từ @WedlyPrairie, tôi không thể thấy cách điều này thậm chí giải quyết bất kỳ phần nào của câu hỏi đã được trình bày. "N địa chỉ hàng đầu và N cuốn sách hàng đầu trên mỗi địa chỉ".
Neil Lunn

làm ơn giúp đỡ nếu bạn có thể cho các câu hỏi liên quan trong mongoDB - stackoverflow.com/questions/61067856/…
newdeveloper

4

Truy vấn dưới đây sẽ cung cấp chính xác kết quả giống như được đưa ra trong phản hồi mong muốn:

db.books.aggregate([
    {
        $group: {
            _id: { addresses: "$addr", books: "$book" },
            num: { $sum :1 }
        }
    },
    {
        $group: {
            _id: "$_id.addresses",
            bookCounts: { $push: { bookName: "$_id.books",count: "$num" } }
        }
    },
    {
        $project: {
            _id: 1,
            bookCounts:1,
            "totalBookAtAddress": {
                "$sum": "$bookCounts.count"
            }
        }
    }

]) 

Câu trả lời sẽ như sau:

/* 1 */
{
    "_id" : "address4",
    "bookCounts" : [
        {
            "bookName" : "book3",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
},

/* 2 */
{
    "_id" : "address90",
    "bookCounts" : [
        {
            "bookName" : "book33",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
},

/* 3 */
{
    "_id" : "address15",
    "bookCounts" : [
        {
            "bookName" : "book1",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
},

/* 4 */
{
    "_id" : "address3",
    "bookCounts" : [
        {
            "bookName" : "book9",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
},

/* 5 */
{
    "_id" : "address5",
    "bookCounts" : [
        {
            "bookName" : "book1",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
},

/* 6 */
{
    "_id" : "address1",
    "bookCounts" : [
        {
            "bookName" : "book1",
            "count" : 3
        },
        {
            "bookName" : "book5",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 4
},

/* 7 */
{
    "_id" : "address2",
    "bookCounts" : [
        {
            "bookName" : "book1",
            "count" : 2
        },
        {
            "bookName" : "book5",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 3
},

/* 8 */
{
    "_id" : "address77",
    "bookCounts" : [
        {
            "bookName" : "book11",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
},

/* 9 */
{
    "_id" : "address9",
    "bookCounts" : [
        {
            "bookName" : "book99",
            "count" : 1
        }
    ],
    "totalBookAtAddress" : 1
}

Tất nhiên nó không trả lại "phản hồi mong muốn". Bạn chưa đọc câu hỏi hoặc câu trả lời được chấp nhận. Câu hỏi yêu cầu N kết quả hàng đầu cho mỗi nhóm, không chỉ TẤT CẢ các kết quả. Hãy xem kỹ phản hồi một lần nữa (đặc biệt là hai danh sách đầu tiên). Chúng tôi chỉ trả lại hai "mặt hàng hàng đầu" trong mỗi nhóm
Neil Lunn
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.