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

MongoDB聚合管道复杂查询实例

2021-07-247.0k 阅读

MongoDB聚合管道基础回顾

在深入复杂查询实例之前,我们先简单回顾一下MongoDB聚合管道的基础概念。聚合操作是MongoDB中用于处理数据的强大工具,它允许我们对集合中的文档进行复杂的数据处理和分析,最终返回计算后的数据结果。

聚合管道由多个阶段(stages)组成,每个阶段对输入文档执行特定的操作,并将输出作为下一个阶段的输入。这些阶段按顺序排列,形成一个数据处理的管道。例如,$match阶段用于筛选文档,$group阶段用于分组文档并进行聚合计算,$project阶段用于修改输出文档的结构等。

下面是一个简单的聚合管道示例,用于计算orders集合中每个客户的订单总金额:

db.orders.aggregate([
    {
        $group: {
            _id: "$customerId",
            totalAmount: { $sum: "$amount" }
        }
    }
]);

在这个示例中,$group阶段按customerId对文档进行分组,并使用$sum操作符计算每个组中amount字段的总和。

复杂查询实例

实例一:多条件筛选与分组统计

假设我们有一个电商数据库,其中有一个products集合,包含产品的各种信息,如name(产品名称)、category(产品类别)、price(价格)、quantity(库存数量)等字段。我们需要统计每个类别中价格高于某个阈值且库存数量大于一定值的产品的平均价格和总库存数量。

  1. 需求分析

    • 首先要筛选出满足价格和库存数量条件的产品文档。
    • 然后按类别对筛选后的文档进行分组。
    • 对每个组计算平均价格和总库存数量。
  2. 代码实现

db.products.aggregate([
    {
        // 筛选价格高于100且库存数量大于50的产品
        $match: {
            price: { $gt: 100 },
            quantity: { $gt: 50 }
        }
    },
    {
        // 按类别分组
        $group: {
            _id: "$category",
            averagePrice: { $avg: "$price" },
            totalQuantity: { $sum: "$quantity" }
        }
    }
]);
  1. 代码解析
    • $match阶段使用$gt操作符筛选出价格大于100且库存数量大于50的产品文档。
    • $group阶段按category字段对筛选后的文档进行分组。$avg操作符计算每个组中产品的平均价格,$sum操作符计算每个组中产品的总库存数量。

实例二:嵌套文档处理与复杂统计

假设我们有一个employees集合,每个员工文档包含基本信息(如namedepartment)以及一个projects数组,数组中的每个元素是一个嵌套文档,包含项目名称(projectName)、项目开始日期(startDate)和项目结束日期(endDate)。我们需要统计每个部门员工参与的项目数量,以及每个部门员工参与项目的平均时长(按天计算)。

  1. 需求分析

    • 首先要展开projects数组,使每个项目成为一个独立的文档。
    • 计算每个项目的时长(结束日期减去开始日期)。
    • 按部门对文档进行分组,并统计每个部门的项目数量和平均项目时长。
  2. 代码实现

db.employees.aggregate([
    {
        // 展开projects数组
        $unwind: "$projects"
    },
    {
        // 计算项目时长(假设日期格式为ISODate)
        $addFields: {
            projectDuration: {
                $divide: [
                    { $subtract: ["$projects.endDate", "$projects.startDate"] },
                    1000 * 60 * 60 * 24
                ]
            }
        }
    },
    {
        // 按部门分组
        $group: {
            _id: "$department",
            projectCount: { $sum: 1 },
            averageDuration: { $avg: "$projectDuration" }
        }
    }
]);
  1. 代码解析
    • $unwind阶段将projects数组展开,使每个数组元素成为一个独立的文档。
    • $addFields阶段使用$subtract操作符计算项目的时长(以毫秒为单位),然后通过$divide操作符将其转换为天数。
    • $group阶段按department字段对文档进行分组。$sum: 1统计每个部门的项目数量,$avg计算每个部门项目的平均时长。

实例三:多集合关联与复杂聚合

假设我们有两个集合:orders集合和products集合。orders集合包含订单信息,如orderIdcustomerIdproductIdquantity等字段;products集合包含产品信息,如productIdnameprice等字段。我们需要统计每个客户购买的产品总金额,同时显示产品名称。

  1. 需求分析

    • 首先要将orders集合和products集合通过productId进行关联。
    • 计算每个订单的金额(数量乘以产品价格)。
    • 按客户对订单金额进行分组,统计每个客户的总购买金额,并显示购买的产品名称。
  2. 代码实现

db.orders.aggregate([
    {
        // 与products集合进行左外连接
        $lookup: {
            from: "products",
            localField: "productId",
            foreignField: "productId",
            as: "productInfo"
        }
    },
    {
        // 展开productInfo数组
        $unwind: "$productInfo"
    },
    {
        // 计算每个订单的金额
        $addFields: {
            orderAmount: { $multiply: ["$quantity", "$productInfo.price"] }
        }
    },
    {
        // 按客户分组
        $group: {
            _id: "$customerId",
            totalAmount: { $sum: "$orderAmount" },
            productNames: { $push: "$productInfo.name" }
        }
    }
]);
  1. 代码解析
    • $lookup阶段使用左外连接将orders集合和products集合通过productId关联起来,结果存储在productInfo数组中。
    • $unwind阶段展开productInfo数组。
    • $addFields阶段计算每个订单的金额。
    • $group阶段按customerId对文档进行分组。$sum计算每个客户的总购买金额,$push操作符将购买的产品名称存储在productNames数组中。

实例四:子文档聚合与复杂过滤

假设我们有一个students集合,每个学生文档包含基本信息(如nameage)以及一个grades子文档数组,每个子文档包含课程名称(course)和成绩(score)。我们需要找出平均成绩大于80分且至少有一门课程成绩大于90分的学生。

  1. 需求分析

    • 首先要计算每个学生的平均成绩。
    • 然后筛选出平均成绩大于80分的学生。
    • 接着检查每个学生是否至少有一门课程成绩大于90分。
  2. 代码实现

db.students.aggregate([
    {
        // 计算平均成绩
        $addFields: {
            averageScore: {
                $avg: "$grades.score"
            }
        }
    },
    {
        // 筛选平均成绩大于80分的学生
        $match: {
            averageScore: { $gt: 80 }
        }
    },
    {
        // 检查是否至少有一门课程成绩大于90分
        $match: {
            "grades.score": { $gt: 90 }
        }
    }
]);
  1. 代码解析
    • $addFields阶段使用$avg操作符计算每个学生的平均成绩。
    • 第一个$match阶段筛选出平均成绩大于80分的学生。
    • 第二个$match阶段通过查询grades.score数组,检查是否至少有一个成绩大于90分。

实例五:多阶段复杂聚合与数据转换

假设我们有一个sales集合,包含销售记录,每个记录包含销售日期(saleDate)、产品ID(productId)、销售数量(quantity)和销售价格(price)。我们需要按季度统计每个产品的销售总额,并将结果转换为一个包含季度和销售总额的对象数组,同时按照销售总额降序排列。

  1. 需求分析

    • 首先要从销售日期中提取季度信息。
    • 按产品ID和季度对销售记录进行分组,并计算每个组的销售总额。
    • 将分组结果转换为特定格式的对象数组。
    • 按销售总额降序排列。
  2. 代码实现

db.sales.aggregate([
    {
        // 提取季度信息
        $addFields: {
            quarter: {
                $ceil: {
                    $divide: [
                        { $month: "$saleDate" },
                        3
                    ]
                }
            }
        }
    },
    {
        // 按产品ID和季度分组,计算销售总额
        $group: {
            _id: {
                productId: "$productId",
                quarter: "$quarter"
            },
            totalSales: { $sum: { $multiply: ["$quantity", "$price"] } }
        }
    },
    {
        // 将结果转换为特定格式的对象数组
        $group: {
            _id: "$_id.productId",
            salesByQuarter: {
                $push: {
                    quarter: "$_id.quarter",
                    totalSales: "$totalSales"
                }
            }
        }
    },
    {
        // 按销售总额降序排列
        $addFields: {
            sortedSalesByQuarter: {
                $sortArray: {
                    input: "$salesByQuarter",
                    sortBy: { totalSales: -1 }
                }
            }
        }
    },
    {
        // 只保留排序后的数组
        $project: {
            salesByQuarter: "$sortedSalesByQuarter",
            _id: 0
        }
    }
]);
  1. 代码解析
    • $addFields阶段通过$month操作符提取销售日期中的月份,再通过$divide$ceil操作符计算出季度。
    • 第一个$group阶段按产品ID和季度对销售记录进行分组,并使用$sum$multiply操作符计算每个组的销售总额。
    • 第二个$group阶段将分组结果转换为包含季度和销售总额的对象数组。
    • 再次使用$addFields阶段,通过$sortArray操作符按销售总额降序排列对象数组。
    • 最后,$project阶段只保留排序后的数组,并去掉_id字段。

实例六:嵌套聚合与条件计算

假设我们有一个companies集合,每个公司文档包含公司名称(name)、员工列表(employees),员工列表中的每个员工是一个包含姓名(employeeName)和工资(salary)的文档。我们需要计算每个公司的平均工资,并且如果平均工资高于10000,则标记为“High Pay”,否则标记为“Normal Pay”。

  1. 需求分析

    • 首先要计算每个公司的平均工资。
    • 然后根据平均工资进行条件判断并标记。
  2. 代码实现

db.companies.aggregate([
    {
        // 计算平均工资
        $addFields: {
            averageSalary: {
                $avg: "$employees.salary"
            }
        }
    },
    {
        // 根据平均工资进行条件判断并标记
        $addFields: {
            payLevel: {
                $cond: {
                    if: { $gt: ["$averageSalary", 10000] },
                    then: "High Pay",
                    else: "Normal Pay"
                }
            }
        }
    }
]);
  1. 代码解析
    • $addFields阶段使用$avg操作符计算每个公司员工的平均工资。
    • 第二个$addFields阶段使用$cond操作符进行条件判断。如果平均工资大于10000,则标记为“High Pay”,否则标记为“Normal Pay”。

实例七:复杂地理空间聚合

假设我们有一个stores集合,每个商店文档包含商店名称(name)、位置(location,格式为GeoJSON)。我们需要找出距离某个特定坐标点(例如[longitude, latitude])最近的5家商店,并计算这5家商店的平均距离。

  1. 需求分析

    • 首先要计算每个商店到特定坐标点的距离。
    • 然后筛选出距离最近的5家商店。
    • 最后计算这5家商店的平均距离。
  2. 代码实现

const targetLocation = [longitude, latitude];

db.stores.aggregate([
    {
        // 计算到目标位置的距离(单位:米)
        $addFields: {
            distance: {
                $geoNear: {
                    near: { type: "Point", coordinates: targetLocation },
                    spherical: true,
                    distanceField: "distance"
                }
            }
        }
    },
    {
        // 按距离升序排序
        $sort: {
            distance: 1
        }
    },
    {
        // 只取前5家商店
        $limit: 5
    },
    {
        // 计算平均距离
        $group: {
            _id: null,
            averageDistance: { $avg: "$distance" }
        }
    }
]);
  1. 代码解析
    • $addFields阶段使用$geoNear操作符计算每个商店到目标位置的距离,并将结果存储在distance字段中。
    • $sort阶段按距离升序排序。
    • $limit阶段只取距离最近的5家商店。
    • $group阶段计算这5家商店的平均距离,_id: null表示将所有文档聚合为一组。

实例八:结合文本搜索与聚合

假设我们有一个articles集合,每个文章文档包含标题(title)、正文(content)和发布日期(publishedDate)。我们需要搜索标题或正文中包含特定关键词的文章,并按发布日期降序排列,同时统计搜索结果中不同年份发布的文章数量。

  1. 需求分析

    • 首先要进行文本搜索,找出标题或正文中包含特定关键词的文章。
    • 按发布日期降序排列搜索结果。
    • 按文章发布年份对结果进行分组,并统计每个年份的文章数量。
  2. 代码实现

const keyword = "特定关键词";

db.articles.aggregate([
    {
        // 文本搜索
        $match: {
            $text: {
                $search: keyword
            }
        }
    },
    {
        // 按发布日期降序排序
        $sort: {
            publishedDate: -1
        }
    },
    {
        // 提取发布年份
        $addFields: {
            publishYear: { $year: "$publishedDate" }
        }
    },
    {
        // 按年份分组并统计文章数量
        $group: {
            _id: "$publishYear",
            articleCount: { $sum: 1 }
        }
    }
]);
  1. 代码解析
    • $match阶段使用$text操作符进行文本搜索,找出标题或正文中包含特定关键词的文章。
    • $sort阶段按发布日期降序排列搜索结果。
    • $addFields阶段通过$year操作符提取文章发布年份。
    • $group阶段按年份对文章进行分组,并使用$sum: 1统计每个年份的文章数量。

通过以上这些复杂查询实例,我们可以看到MongoDB聚合管道在处理各种复杂数据场景时的强大能力。掌握这些技巧,能够帮助开发者更高效地从MongoDB数据库中提取有价值的信息。在实际应用中,可能还会遇到更复杂的业务需求,需要灵活组合和运用聚合管道的各个阶段及操作符来解决问题。