MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MongoDB聚合框架表达式全解析

2023-03-085.0k 阅读

1. 聚合框架概述

MongoDB的聚合框架是一种强大的数据处理工具,它允许我们对集合中的文档进行复杂的数据处理操作,如数据分组、过滤、转换和汇总等。聚合操作以管道的形式进行,文档在管道中逐个阶段处理,每个阶段对输入文档执行特定操作,并将结果输出到下一个阶段。这种链式处理方式使得我们能够灵活地构建复杂的数据处理逻辑。

2. 聚合框架的基本语法

聚合操作通常使用aggregate方法,语法如下:

db.collection.aggregate([
    { <stage1> },
    { <stage2> },
    //... 更多阶段
    { <stageN> }
])

每个阶段(<stage>)都是一个文档,包含特定的操作符和参数。例如,$match阶段用于过滤文档,$group阶段用于分组和汇总数据。

3. 常用聚合表达式

3.1 $project 表达式

$project阶段用于修改输出文档的结构,我们可以选择包含或排除某些字段,重命名字段,甚至创建新的计算字段。

  • 选择和排除字段
    • 要选择特定字段并排除其他字段,可以在$project中列出需要的字段,并将其值设为1。例如,假设我们有一个students集合,包含nameagegrades字段,要仅输出nameage字段:
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
        }
    }
])
  • 创建计算字段: 我们可以使用各种表达式创建新的计算字段。例如,假设有pricequantity字段,计算总金额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的连接操作。假设我们有两个集合orderscustomersorders集合包含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聚合框架表达式的全面解析,我们可以看到它在数据处理方面的强大功能和灵活性。合理运用这些表达式和操作符,可以高效地完成各种复杂的数据处理任务。在实际应用中,要根据具体需求和数据特点,优化聚合操作,以获得最佳性能。