分組並獲取每個分組中Top N個數據的需求在實際開發的過程中經常會遇到。例如,購物網站中經常會遇到的展示一個店鋪列表,每個店鋪列表中帶有多個該店鋪的產品信息。當然,展示店鋪列表並分別去獲取店鋪指定數量的產品是個最為簡單的做法,但需要消耗大量的資源。
在本文中,我們將會以一個簡單的例子展示在MongoDB中實現分組並獲取Top N個數據的實現方法。
示例
首先,我們在MongoDB中有一個用戶信息的數據集合user,它存有下面的幾條數據。
[ { "name": "劉大", "age": 28, "status": "active" }, { "name": "陳二", "age": 25, "status": "active" }, { "name": "張三", "age": 25, "status": "active" }, { "name": "李四", "age": 25, "status": "active" }, { "name": "王五", "age": 23, "status": "active" }, { "name": "趙六", "age": 23, "status": "active" }, { "name": "孫七", "age": 23, "status": "inactive" }, { "name": "周八", "age": 23, "status": "active" } ]
在以上數據的基礎上,我們准備在每個年齡抽取前兩個(以先添加的文檔為准)狀態為active的人,並以年齡從小到大的形式輸出分組。
首先,我們使用$match運算符進行了篩選,去除了狀態不為active的文檔。根據上面的要求,我們需要按年齡從小到大的形式排序,即使age按升序的形式排序(升序在MongoDB中以1表示)。另外,為了實現每個分組都能取到最先添加的兩個文檔,我們也增加了基於createdAt的升序排序。age的排序也可以在$group后執行,但在這里我們直接與時間排序合並在一起執行。
在篩選並排序后,我們需要使用$group運算符根據指定的字段進行分組。根據要求,我們需要使用age作為分組的依據,所以在實現中我們將_id設置為$age。在分組中,我們希望獲取到各分組中的數組,所以使用了$push運算,將各文檔(使用$$ROOT代表根文檔)保存到products中。完成分組后,每個分組中的products保存了該分組所有的文檔,為了實現獲取TopN個元素,我們需要在$project中使用$slice限制返回的文檔個數。
db.user.aggregate([ { $match: { status: "active", }, }, { $sort: { age: 1, createdAt: 1, }, }, { $group: { _id: "$age", persons: { $push: "$$ROOT", }, }, }, { $project: { _id: 0, age: "$_id", persons: { $slice: ["$persons", 2], }, }, }, ])
執行該查詢,可以得到下面的返回結果:
[ { age: 23, persons: [ { name: "王五", age: 23, status: "active" }, { name: "趙六", age: 23, status: "active" }, ], }, { age: 25, persons: [ { name: "陳二", age: 25, status: "active" }, { name: "張三", age: 25, status: "active" }, ], }, { age: 28, persons: [{ name: "劉大", age: 28, status: "active" }], }, ]
不分組返回結果
上面的輸出結果中仍保持着分組的形式,如果需要將結果轉換為文檔的數組,可以另外使用$unwind以及$replaceRoot運算符。例如下面的例子:
db.user.aggregate([ // $match, $sort, $group, $project { $unwind: "$persons", }, { $replaceRoot: { newRoot: "$persons", }, }, ])
該查詢執行后得到的結果為:
[ { name: "王五", age: 23, status: "active" }, { name: "趙六", age: 23, status: "active" }, { name: "陳二", age: 25, status: "active" }, { name: "張三", age: 25, status: "active" }, { name: "劉大", age: 28, status: "active" }, ]
$slice查詢修飾符
$slice元素會限制數組元素的個數
查詢輸出數組前三個:
db.movies.find({title:"Youth Without Youth"},{languages:{$slice:3}}).pretty();
查詢輸出數組后兩個
db.movies.find({title:"Youth Without Youth"},{languages:{$slice:-2}}).pretty();
查詢輸出從數組第四個開始,輸出前三個
db.movies.find({title:"Youth Without Youth"},{languages:{$slice:[3,3]}}).pretty();
查詢輸出從數組倒數第五個開始,輸出前四個
db.movies.find({title:"Youth Without Youth"},{languages:{$slice:[-5,4]}}).pretty();
$slice:[a,b]
a是正數時,起始位置是a+1,
a是負數時,起始位置a(倒數數組)
b是輸出個數