MongoDB聚合框架表达式全解析
1. 聚合框架概述
MongoDB的聚合框架是一种强大的数据处理工具,它允许我们对集合中的文档进行复杂的数据处理操作,如数据分组、过滤、转换和汇总等。聚合操作以管道的形式进行,文档在管道中逐个阶段处理,每个阶段对输入文档执行特定操作,并将结果输出到下一个阶段。这种链式处理方式使得我们能够灵活地构建复杂的数据处理逻辑。
2. 聚合框架的基本语法
聚合操作通常使用aggregate
方法,语法如下:
db.collection.aggregate([
{ <stage1> },
{ <stage2> },
//... 更多阶段
{ <stageN> }
])
每个阶段(<stage>
)都是一个文档,包含特定的操作符和参数。例如,$match
阶段用于过滤文档,$group
阶段用于分组和汇总数据。
3. 常用聚合表达式
3.1 $project
表达式
$project
阶段用于修改输出文档的结构,我们可以选择包含或排除某些字段,重命名字段,甚至创建新的计算字段。
- 选择和排除字段:
- 要选择特定字段并排除其他字段,可以在
$project
中列出需要的字段,并将其值设为1。例如,假设我们有一个students
集合,包含name
、age
和grades
字段,要仅输出name
和age
字段:
- 要选择特定字段并排除其他字段,可以在
db.students.aggregate([
{
$project: {
name: 1,
age: 1
}
}
])
- 要排除特定字段,可以将其值设为0。例如,排除
grades
字段:
db.students.aggregate([
{
$project: {
grades: 0
}
}
])
- 重命名字段:
可以通过指定新的字段名和旧的字段名来重命名字段。例如,将
name
字段重命名为student_name
:
db.students.aggregate([
{
$project: {
student_name: "$name",
age: 1
}
}
])
- 创建计算字段:
我们可以使用各种表达式创建新的计算字段。例如,假设有
price
和quantity
字段,计算总金额total_amount
:
db.products.aggregate([
{
$project: {
product_name: 1,
price: 1,
quantity: 1,
total_amount: { $multiply: ["$price", "$quantity"] }
}
}
])
这里$multiply
是一个算术表达式,用于将$price
和$quantity
相乘。
3.2 $match
表达式
$match
阶段用于过滤文档,仅允许符合指定条件的文档进入下一个阶段。它使用与find
方法类似的查询语法。
- 简单条件过滤:
例如,在
students
集合中查找年龄大于18岁的学生:
db.students.aggregate([
{
$match: {
age: { $gt: 18 }
}
}
])
这里$gt
是比较操作符,表示“大于”。
- 多条件过滤:
可以使用逻辑操作符(如
$and
、$or
)组合多个条件。例如,查找年龄大于18岁且成绩大于80分的学生:
db.students.aggregate([
{
$match: {
$and: [
{ age: { $gt: 18 } },
{ grades: { $gt: 80 } }
]
}
}
])
3.3 $group
表达式
$group
阶段用于按指定的字段对文档进行分组,并对每个组执行汇总操作。
- 基本分组:
例如,在
orders
集合中,按customer_id
分组并计算每个客户的订单总数:
db.orders.aggregate([
{
$group: {
_id: "$customer_id",
order_count: { $sum: 1 }
}
}
])
这里_id
字段指定了分组依据,$sum
是一个累加操作符,1表示每个文档计数为1。
- 复杂分组和汇总:
假设
orders
集合还有order_amount
字段,我们可以在分组后计算每个客户的总订单金额:
db.orders.aggregate([
{
$group: {
_id: "$customer_id",
order_count: { $sum: 1 },
total_amount: { $sum: "$order_amount" }
}
}
])
3.4 $sort
表达式
$sort
阶段用于按指定字段对文档进行排序。
- 升序排序:
例如,在
students
集合中按年龄升序排序:
db.students.aggregate([
{
$sort: {
age: 1
}
}
])
这里1表示升序排序。
- 降序排序: 按成绩降序排序:
db.students.aggregate([
{
$sort: {
grades: -1
}
}
])
这里 -1 表示降序排序。
- 多字段排序: 可以按多个字段排序,例如先按年龄升序,年龄相同再按成绩降序:
db.students.aggregate([
{
$sort: {
age: 1,
grades: -1
}
}
])
3.5 $limit
表达式
$limit
阶段用于限制输出结果的文档数量。例如,只返回students
集合中的前10个学生:
db.students.aggregate([
{
$limit: 10
}
])
3.6 $skip
表达式
$skip
阶段用于跳过指定数量的文档。例如,跳过students
集合中的前5个学生,返回后面的学生:
db.students.aggregate([
{
$skip: 5
}
])
3.7 $unwind
表达式
$unwind
阶段用于将数组类型的字段展开为单个文档。假设students
集合中的hobbies
字段是一个数组,包含每个学生的多个爱好:
db.students.aggregate([
{
$unwind: "$hobbies"
}
])
这样每个学生的每个爱好都会成为一个单独的文档,便于后续处理。
4. 高级聚合表达式
4.1 $lookup
表达式(表连接)
$lookup
阶段用于在两个集合之间执行类似于SQL的连接操作。假设我们有两个集合orders
和customers
,orders
集合包含customer_id
字段,customers
集合包含_id
(代表客户ID)和customer_name
等字段。我们要在orders
集合中加入客户的名称信息:
db.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customer_id",
foreignField: "_id",
as: "customer_info"
}
}
])
这里from
指定要连接的集合,localField
是当前集合中的字段,foreignField
是被连接集合中的字段,as
指定连接结果的输出字段名。
4.2 $facet
表达式
$facet
阶段允许我们在单个聚合管道中并行执行多个聚合操作,并将结果合并到一个文档中。例如,假设我们要同时获取students
集合中年龄的统计信息(平均年龄、最大年龄、最小年龄)以及按成绩分组的学生数量:
db.students.aggregate([
{
$facet: {
age_stats: [
{
$group: {
_id: null,
avg_age: { $avg: "$age" },
max_age: { $max: "$age" },
min_age: { $min: "$age" }
}
}
],
grade_groups: [
{
$group: {
_id: "$grades",
student_count: { $sum: 1 }
}
}
]
}
}
])
这样我们可以在一次聚合操作中得到不同维度的统计结果。
4.3 $bucket
表达式
$bucket
阶段用于根据指定的边界条件将文档分组到多个桶(bucket)中。例如,假设我们要将students
集合中的学生按年龄范围分组:
db.students.aggregate([
{
$bucket: {
groupBy: "$age",
boundaries: [10, 20, 30],
output: {
student_count: { $sum: 1 }
}
}
}
])
这里groupBy
指定分组字段,boundaries
定义桶的边界,output
定义每个桶内的汇总操作。
4.4 $bucketAuto
表达式
与$bucket
类似,但$bucketAuto
会自动根据数据分布确定桶的边界。例如:
db.students.aggregate([
{
$bucketAuto: {
groupBy: "$age",
buckets: 3,
output: {
student_count: { $sum: 1 }
}
}
}
])
这里buckets
指定桶的数量,MongoDB会根据age
字段的数据自动划分桶。
5. 表达式中的操作符
5.1 算术操作符
$add
:用于加法运算。例如,计算两个字段的和:
db.products.aggregate([
{
$project: {
new_price: { $add: ["$price", "$tax"] }
}
}
])
$subtract
:减法运算。例如,计算价格减去折扣后的实际价格:
db.products.aggregate([
{
$project: {
actual_price: { $subtract: ["$price", "$discount"] }
}
}
])
$multiply
:乘法运算,前文已有示例。$divide
:除法运算。例如,计算每件商品的平均成本:
db.products.aggregate([
{
$project: {
avg_cost: { $divide: ["$total_cost", "$quantity"] }
}
}
])
5.2 比较操作符
$eq
:判断两个值是否相等。例如,在employees
集合中查找工资等于5000的员工:
db.employees.aggregate([
{
$match: {
salary: { $eq: 5000 }
}
}
])
$ne
:判断两个值是否不相等。$gt
、$gte
、$lt
、$lte
:大于、大于等于、小于、小于等于比较操作符,前文已有示例。
5.3 逻辑操作符
$and
:逻辑与操作,前文已有示例。$or
:逻辑或操作。例如,查找年龄大于30岁或者工资大于8000的员工:
db.employees.aggregate([
{
$match: {
$or: [
{ age: { $gt: 30 } },
{ salary: { $gt: 8000 } }
]
}
}
])
$not
:逻辑非操作。例如,查找年龄不大于30岁的员工:
db.employees.aggregate([
{
$match: {
age: { $not: { $gt: 30 } }
}
}
])
5.4 数组操作符
$in
:判断一个值是否在数组中。例如,在students
集合中查找爱好在指定数组中的学生:
db.students.aggregate([
{
$match: {
hobbies: { $in: ["reading", "swimming"] }
}
}
])
$size
:获取数组的大小。例如,计算每个学生的爱好数量:
db.students.aggregate([
{
$project: {
hobby_count: { $size: "$hobbies" }
}
}
])
6. 聚合框架的性能优化
- 合理使用索引:在
$match
阶段,如果过滤条件字段上有索引,会大大提高聚合操作的性能。例如,在students
集合中按age
字段过滤,如果age
字段有索引,查询速度会加快。
db.students.createIndex({ age: 1 })
- 减少数据传输:尽量在聚合管道的早期阶段进行过滤(如使用
$match
),减少进入后续阶段的数据量,从而减少内存和网络开销。 - 避免复杂嵌套:复杂的嵌套聚合操作可能会导致性能问题,尽量将复杂逻辑分解为多个简单的聚合阶段。
7. 实战案例
假设我们有一个电商数据库,包含products
(商品)、orders
(订单)和customers
(客户)集合。
- 需求1:统计每个客户购买的商品总金额,并按总金额降序排序。
db.orders.aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
},
{
$unwind: "$product_info"
},
{
$group: {
_id: "$customer_id",
total_amount: { $sum: { $multiply: ["$quantity", "$product_info.price"] } }
}
},
{
$sort: {
total_amount: -1
}
}
])
- 需求2:查找购买过特定商品(如商品ID为 "product123")的客户信息。
db.orders.aggregate([
{
$match: {
product_id: "product123"
}
},
{
$lookup: {
from: "customers",
localField: "customer_id",
foreignField: "_id",
as: "customer_info"
}
},
{
$unwind: "$customer_info"
},
{
$project: {
customer_info: 1
}
}
])
通过以上对MongoDB聚合框架表达式的全面解析,我们可以看到它在数据处理方面的强大功能和灵活性。合理运用这些表达式和操作符,可以高效地完成各种复杂的数据处理任务。在实际应用中,要根据具体需求和数据特点,优化聚合操作,以获得最佳性能。