前言
前面已经对MongoDB基础的增删改查进行了分享,在增删改方面基本已经够用了,但是查询方面是远远不够的,find方法只能传入一些查询条件。涉及到稍微复杂点的查询就无法实现了,如分组、排序、映射(起别名)等这些操作的时候就没有办法实现。
所以我接下来给同志们分享专一用于MongoDB复杂查询的框架 ----- 聚合框架
注意: 文档和JSON对象基本上是一个意思,我会在解释的时候说是JSON对象,便于理解,在写标题的写的是文档,文档是正式的概念
如果对基础的查询条件不熟悉的同志,可以查看一下我前面写的文章
目录
[条件过滤 match](#条件过滤 match)
[字段选择 project](#字段选择 project)
[分组统计 group](#分组统计 group)
[排序 sort](#排序 sort)
[限制结果数 limit和跳过文档skip](#限制结果数 limit和跳过文档skip)
[左连接 lookup和拆分数组字段为多个文档unwind](#左连接 lookup和拆分数组字段为多个文档unwind)
聚合框架介绍
MongoDB的聚合框架是一个基于管道概念的强大工具,它允许用户对集合中的文档进行一系列复杂的数据转换、过滤、分组、排序和计算操作,最终生成所需的结果。
框架入口
db.集合.aggregate(聚合管道)
聚合管道
下面我介绍一下aggregate方法的参数,聚合管道
这个参数是一个数组,数组里面记录的是每个聚合阶段,每个聚合阶段把文档进行过滤之后,把结果交给下一个聚合阶段,这就是聚合管道的执行流程
具体的聚合管道的格式
[{操作符1: 操作参数1},{操作符2,操作参数2},{操作符3,操作参数3}...]
常见的操作符
MongoDB的操作符和SQL中的关键字对比,快速入门,类比使用
功能 | MongoDB 操作符 | SQL 关键字/语法 |
---|---|---|
条件过滤 | $match |
WHERE |
字段选择 | $project |
SELECT 字段列表 |
分组统计 | $group |
GROUP BY + 聚合函数 |
排序 | $sort |
ORDER BY |
限制结果数 | $limit |
LIMIT |
跳过文档 | $skip |
OFFSET |
左外连接 | $lookup |
LEFT JOIN |
将数组字段拆分为多条文档 | $unwind | 配合MongoDB左连接后匹配多个右表数据,此时的匹配到的右表数据是以数组的形式添加到左表中的,$unwinds是为了展开这些右表数据的,是MongoDB特有的 |
常用操作符的操作参数介绍
条件过滤 $match
条件过滤 $match 类比 SQL中的where关键字
操作参数格式
$match操作符的操作参数格式
{查询条件}
查询条件本质就是逻辑操作符和查询操作符的嵌套
查询操作符和逻辑操作符常用列表如下
查询操作符
操作符 | 说明 | 示例 |
---|---|---|
$eq |
等于 | age: { $eq: 25 } |
$ne |
不等于 | age: { $ne: 25 } |
$gt |
大于 | age: { $gt: 25 } |
$lt |
小于 | age: { $lt: 25 } |
$in |
包含在数组中 | name: { $in: ["Alice", "Bob"] } |
$regex |
正则匹配 | name: { $regex: /^A/ } |
逻辑操作符
| 操作符 | 说明 | 示例 |
| and | 与 | 显式写法 and: [{查询操作符1}, {查询操作符2}] (推荐)隐式写法 查询操作符1,查询操作符2 |
| or | 或 | or: [{查询操作符1}, {查询操作符2}] |
| not | 单个取反 | not: {查询操作符} |
$nor |
多个取反 | $nor: [{查询操作符1}, {查询操作符2}] |
---|
如果and和or都出现的情况下,推荐使用隐式and和显式or搭配使用,清晰明了
案例解析
#案例: 查询status状态是completed 并且 total_price大于500
# MongoDB的语句
{ $match: { status: { $eq: "completed" }, total_price: { $gt: 500 } } }
#对应的SQL语句
WHERE status = 'completed' AND total_price > 500
字段选择 $project
字段选择 $project 类比 select的字段列表
操作参数格式
$project操作符的操作参数格式
# 大概的格式如下
{key1: value2, key1: value2...}
# key: value 键值对的写法有三种,保留字段和排除字段不能同时使用
# _id字段是默认保留的,所以_id字段是可以在保留字段语句中显示排除的
# 第一种 保留字段,1代表保留字段
字段名: 1
# 第二种 排除字段,0代表排除字段
字段名: 0
# 第三种 保留字段并对字段重命名
字段新名字: "$原字段名"
案例解析
# 案例: 在查询一张表的时候,保留order_id、cust_id、uname字段,并且把uname重命名为username
# MongoDB的语句
{$project: {_id: 0, order_id: 1, cust_id: 1, username: "$uname"}}
# 对应的SQL语句
SELECT order_id, cust_id, uname as username
分组统计 $group
分组统计 $group 类比 GROUP BY
+ 聚合函数
操作参数格式
$group操作符的操作参数格式
{
$group: {
_id: {$分组字段1,$分组字段2,$分组字段3...},
聚合字段名称1: {聚合函数1},
聚合字段名称2: {聚合函数2},
聚合字段名称3: {聚合函数3},
...
}
}
#分组字段只有一个的时候,形式为: _id: $分组字段
常见的聚合函数
功能 | MongoDB 操作符 | SQL 关键字/语法 |
---|---|---|
计数 | $sum: 1 |
COUNT(*) |
字段名的总和 | $sum: $字段名 |
SUM(字段名) |
字段名的平均值 | $avg: $字段名 |
AVG(字段名) |
字段名的最大值 | $max: $字段名 |
MAX(字段名) |
案例解析
#案例: 在表中使用country字段进行分组统计,统计出每组的行数,每组的平均年龄
# MongoDB的语句
{
$group: {
_id: "$country",
totalUsers: { $sum: 1 },
avgAge: { $avg: "$age" }
}
}
# 对应的SQL语句
select country,count(*) as totalUsers,AVG(age) as avgAge
from 表名
group by country
排序 $sort
排序 $sort
类比 order by
操作参数格式
$sort``操作符的操作参数格式
# key是指排序的字段名 value中1是升序,-1是降序
{ $sort: { key1: value1, key2: value2, ... } }
案例解析
# 案例,对查询到的数据先age字段降序排序,再按price字段升序排序
# MongoDB的语句
{ $sort: { age: -1, price: 1 } }
# SQL语句
ORDER BY age DESC, price ASC
# SQL中的用法比较固定,只能在数据查询的结果中进行排序
# 但是MongoDB中聚合查询是管道过滤的形式,只考虑拿着现有的数据进行排序过滤,因此可以在任何一步进行操作
限制结果数 limit和跳过文档skip
限制结果数 $limit 类比 limit
跳过文档数 $skip 类比 offset
操作参数格式
$limit``操作符和$skip的操作参数格式
{ $limit: 返回前多少JSON对象的个数 }
{ $skip: 跳过前多少JSON对象的个数 }
# 这两个数据常常搭配用于分页
# 在SQL中类似
limit 返回前多少行的个数
offset 跳过前多少行的个数
SQL分页形式是 limit 返回前多少行的个数 offset 跳过前多少行的个数
经常简写为 limit 返回前多少行的个数 跳过前多少行的个数
案例解析
# 例如: SQL语句,跳过前20行,取接下来的10行
limit 10 20
# 写成MongoDB语句,是有顺序的,需要先跳过,在取值
{$skip: 20},{$limit: 10}
左连接 lookup和拆分数组字段为多个文档unwind
左连接 $lookup 类比 left join
拆分数组字段为多个文档$unwind 则是为了后续连接之后的数组拆分的过程
这两个是搭配使用的
操作参数格式
lookup操作符和unwind操作符的格式
# $lookup的格式
# MongoDB语句
{
$lookup: {
from: "右表名称",
localField: "左表的关联字段",
foreignField: "右表关联字段",
as: "匹配到的多条右表JSON对象的数组名称,这里自己自定义取名"
}
}
对应的SQL语句
from 左表名称 left join 右表名称 on 左表的关联字段=右表关联字段
# $unwind的格式
# 基础格式
{ $unwind: "需要展开的数组名称" }
# 带有选项的格式
{ $unwind: {path: "需要展开的数组名称", 选项键值对}}
# 常用的选项键值对
preserveNullAndEmptyArrays: true # 控制是否保留空数组或 null 值的文档(默认 false,即过滤掉)
includeArrayIndex: "添加存储原数组元素的索引位置,自定义字段名称"
等等
案例解析
# 案例: 左表匹配多条右表的例子
# 场景说明
# 左表(orders):一条订单可能关联多个客户(例如"团购订单"或"家庭订单")
# 右表(customers):多个客户可能属于同一个订单(通过 order_id 关联)
# 原始数据
# orders 集合(左表)
# { "_id": 1, "order_id": 1001, "amount": 500 } // 订单 1001
# { "_id": 2, "order_id": 1002, "amount": 300 } // 订单 1002(无关联客户)
# customers 集合(右表)
# { "_id": 200, "name": "Alice", "email": "alice@example.com", "order_id": 1001 } // 属于订单 1001
# { "_id": 201, "name": "Bob", "email": "bob@example.com", "order_id": 1001 } // 属于订单 1001
# { "_id": 202, "name": "Charlie", "email": "charlie@example.com", "order_id": 1001 } // 属于订单 1001
# { "_id": 203, "name": "Dave", "email": "dave@example.com", "order_id": 999 } // 无关联订单
# 执行 $lookup(左表匹配右表的多条记录)
db.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "order_id", // 左表的 order_id
foreignField: "order_id", // 右表的 order_id
as: "customer_info" // 匹配的客户信息存入数组,自定义的名称
}
}
])
# 匹配结果(customer_info 是数组,可能包含多条客户数据)
#订单 1001 匹配到 3 个客户(Alice、Bob、Charlie)
{
"_id": 1,
"order_id": 1001,
"amount": 500,
"customer_info": [
{ "_id": 200, "name": "Alice", "email": "alice@example.com", "order_id":1001 },
{ "_id": 201, "name": "Bob", "email": "bob@example.com", "order_id":1001 },
{ "_id": 202, "name": "Charlie", "email": "charlie@example.com", "order_id":1001}
]
}
#订单 1002 无匹配客户
{
"_id": 2,
"order_id": 1002,
"amount": 300,
"customer_info": [] // 空数组
}
# 执行 $unwind(展开多条客户记录)
{
$unwind: {
path: "$customer_info",
preserveNullAndEmptyArrays: true // 保留无匹配的订单
}
}
# 匹配结果(订单 1001 被展开为 3 条文档,每条对应一个客户)
# 订单 1001 + Alice
{
"_id": 1,
"order_id": 1001,
"amount": 500,
"customer_info": { "_id": 200, "name": "Alice", "email": "alice@example.com", "order_id": 1001 }
}
# 订单 1001 + Bob
{
"_id": 1,
"order_id": 1001,
"amount": 500,
"customer_info": { "_id": 201, "name": "Bob", "email": "bob@example.com", "order_id": 1001 }
}
# 订单 1001 + Charlie
{
"_id": 1,
"order_id": 1001,
"amount": 500,
"customer_info": { "_id": 202, "name": "Charlie", "email": "charlie@example.com", "order_id": 1001 }
}
#订单 1002 无匹配(保留原文档,customer_info 为 null 或不存在)
{
"_id": 2,
"order_id": 1002,
"amount": 300,
"customer_info": null
}
# 最终 $project(提取关键字段)
{
$project: {
order_id: 1,
amount: 1,
customer_name: "$customer_info.name",
customer_email: "$customer_info.email"
}
}
# 最终的处理结果
# 订单 1001 + Alice
{ "order_id": 1001, "amount": 500, "customer_name": "Alice", "customer_email": "alice@example.com" }
# 订单 1001 + Bob
{ "order_id": 1001, "amount": 500, "customer_name": "Bob", "customer_email": "bob@example.com" }
# 订单 1001 + Charlie
{ "order_id": 1001, "amount": 500, "customer_name": "Charlie", "customer_email": "charlie@example.com" }
#订单 1002 无匹配
{ "order_id": 1002, "amount": 300, "customer_name": null, "customer_email": null }