MongoDB聚合框架是一套功能强大且高度灵活的数据处理与分析机制,使用户能够在数据库层直接执行复杂的数据转换与汇总操作。 该框架通过多阶段的数据处理管道,将原始文档作为输入,经过严格定义的处理流程(如过滤、分组、变换与统计等),最终生成满足业务需求的结构化结果数据。

聚合管道(Aggregation Pipeline)是聚合框架的核心概念。它的工作方式非常类似于Unix系统中的管道操作,数据从一个阶段流向下一个阶段,每个阶段都会对数据进行特定的处理。
整个管道处理过程可以理解为一个数据流的加工过程。我们从MongoDB集合中获取文档流,这些文档按顺序通过管道中的每个阶段。每个阶段接收上一个阶段的输出作为自己的输入,经过处理后将结果传递给下一个阶段。这种设计让我们能够将复杂的数据处理任务分解为多个简单、易于理解的步骤。
聚合管道的一个重要特点是它以文档流的形式处理数据。每个阶段都接收文档流作为输入,并输出文档流,这保证了整个处理过程的一致性和可预测性。
聚合管道中的每个阶段都是一个独立的数据处理单元。每个阶段都有自己的职责和功能,就像工厂流水线上的每个工位一样。
阶段处理器负责执行具体的数据处理逻辑,比如筛选满足条件的文档、重新组织文档结构、或者计算统计值。而可调参数(Tunables)则让我们能够精确控制这个阶段的行为。这些参数通常以操作符的形式出现,比如字段修改操作符、算术运算操作符、文档重塑操作符等。
聚合管道的一个强大特性是同类型阶段的重复使用。在实际应用中,我们经常需要在管道的不同位置执行相同类型的操作。
比如说,我们可能需要在管道开始时做一次粗筛选,减少需要处理的数据量以提高效率。然后在经过一些数据转换后,再次进行更精细的筛选,以获得最终需要的结果。这种设计让我们能够构建既高效又灵活的数据处理流程。
|// 示例:多次使用相同类型的阶段 db.sales.aggregate([ { $match: { date: { $gte: new Date("2024-01-01") } } }, // 第一次筛选:按日期 { $lookup: { from: "products", localField: "productId", foreignField: "_id", as: "product" } }, { $match: { "product.category": "electronics" } }, // 第二次筛选:按产品类型 { $group: { _id: "$customer", totalAmount: { $sum: "$amount" } } } ])
理解管道设计的关键在于认识到每个阶段都是独立且可重复的。这种模块化设计让复杂的数据分析任务变得更加容易管理和调试。
聚合管道从指定的MongoDB集合开始工作,集合中的文档按照我们定义的阶段顺序依次处理。每个文档都会经过管道中的每个阶段,除非在某个阶段被过滤掉。
处理完成后,我们得到的输出结果同样是文档流的形式,就像执行普通的find查询一样。这些结果可以用于生成报表、构建Web页面、或者作为其他处理流程的输入数据。
整个过程的美妙之处在于,虽然背后的处理逻辑可能非常复杂,但通过管道的方式,我们能够以直观、易懂的方式来描述和实现这些复杂的数据处理需求。每个阶段都有明确的输入和输出,这让整个处理流程变得清晰可控。
在深入学习聚合框架的高级特性之前,我们先从一些已经熟悉的数据操作开始。这些操作在普通查询中经常使用,在聚合管道中同样发挥着重要作用。通过理解这些基础操作在聚合上下文中的工作方式,我们能够更好地掌握整个聚合框架的设计思想。

为了更好地理解这些操作,我们以一个虚构的公司数据集合为例。这个集合记录了各种创业公司的基本信息,包括公司名称、成立年份、业务描述、融资轮次等详细信息。
|// 示例公司文档结构 { "_id": "64a1b2c3d4e5f6789012345", "name": "智慧科技", "category": "人工智能", "foundedYear": 2020, "description": "专注于机器学习算法研发", "fundingRounds": [ { "roundCode": "seed", "raisedAmount": 5000000, "fundedYear": 2020, "currency"
筛选操作($match)是聚合管道中最基本也是最重要的操作之一。它的作用是从数据流中选出满足特定条件的文档,就像是在原材料中挑选出符合质量标准的部分。
|// 使用聚合管道筛选2020年成立的公司 db.companies.aggregate([ { $match: { foundedYear: 2020 } } ])
这个操作等同于普通查询中的条件筛选,但在聚合管道中,它为后续的数据处理阶段提供了优化的输入。通过在管道早期进行筛选,我们能够显著减少需要处理的数据量,从而提高整体性能。
投影操作($project)让我们能够控制输出文档的结构和内容。这就像是在生产线上对产品进行包装和标记,选择哪些信息需要保留,哪些需要隐藏或转换。
|// 结合筛选和投影,只输出公司名称和成立年份 db.companies.aggregate([ { $match: { foundedYear: 2020 } }, { $project: { _id: 0, // 隐藏ID字段 name: 1, // 保留公司名称 foundedYear: 1 // 保留成立年份 }} ])
这个管道首先筛选出2020年成立的公司,然后重塑文档结构,只保留我们关心的字段。结果会是一个简洁的文档列表,每个文档只包含公司名称和成立年份信息。
投影操作中,使用1表示包含该字段,使用0表示排除该字段。特殊情况是_id字段,它默认会被包含,需要显式设置为0才会被排除。
排序操作(sort)按照指定的字段对文档进行排列,而限制操作(limit)则控制输出结果的数量。这两个操作经常配合使用,比如获取「排名前五的公司」这样的需求。
|// 获取成立年份最新的前5家公司 db.companies.aggregate([ { $match: { foundedYear: 2020 } }, { $sort: { name: 1 } }, // 按公司名称升序排列 { $limit: 5 }, // 限制输出5个结果 { $project: { _id: 0, name: 1 }} ])
在这个例子中,我们首先筛选出2020年成立的公司,然后按公司名称的字母顺序排序,取前5个结果,最后只输出公司名称。
跳过操作($skip)让我们能够跳过指定数量的文档,这在实现分页功能时特别有用。结合排序和限制操作,我们可以轻松实现数据分页。
|// 实现分页:跳过前10个结果,然后取5个 db.companies.aggregate([ { $match: { foundedYear: 2020 } }, { $sort: { name: 1 } }, { $skip: 10 }, // 跳过前10个结果 { $limit: 5 }, // 取接下来的5个 { $project: { _id: 0, name: 1 }} ])
这个管道实现了分页逻辑:在按名称排序的2020年成立的公司中,跳过前10家,显示第11到15家公司的名称。
在构建聚合管道时,各个阶段的顺序对性能和结果都有重要影响。一般来说,我们应该尽早进行筛选操作,以减少后续阶段需要处理的数据量。
|// 推荐的顺序:先筛选,后限制,最后投影 db.companies.aggregate([ { $match: { foundedYear: 2020 } }, // 首先筛选数据 { $limit: 5 }, // 然后限制数量 { $project: { _id: 0, name: 1 }} // 最后选择字段 ])
虽然将投影操作放在限制操作之前也能得到相同的结果,但这会导致更多的文档通过投影阶段,降低整体效率。
MongoDB聚合框架内置了功能全面的表达式体系,涵盖数据转换、计算与逻辑处理等多种需求。
聚合框架中的表达式按照功能可以分为几个主要类别。布尔表达式帮助我们进行逻辑判断,支持与、或、非等逻辑运算。集合表达式让我们能够将数组当作数学集合来处理,执行交集、并集、差集等操作。
比较表达式提供了丰富的比较功能,不仅支持基本的大小比较,还能处理复杂的范围筛选。算术表达式则涵盖了从基础的加减乘除到高级的对数、平方根等数学运算。
字符串表达式专门处理文本数据,提供字符串拼接、子串查找、大小写转换等功能。数组表达式则专注于数组操作,包括数组元素筛选、切片、以及各种数组变换。
变量表达式处理字面值、日期解析和条件判断等场景。累加器表达式则提供了统计计算能力,能够计算总和、平均值、标准差等描述性统计指标。

投影操作不仅仅局限于字段的简单筛选,更是MongoDB中用于灵活调整和重塑文档数据结构的核心手段。通过高级的投影配置,可以精确地实现嵌套字段的提取和映射,将原本复杂的层级结构平展为便于业务处理的扁平文档格式,有效提升数据访问与集成效率。
在实际数据建模及分析场景中,常常需要将多层嵌套的字段提升到文档顶层。此类字段提升操作不仅优化了存取逻辑,也为后续的数据处理和聚合提供了更高效的结构支撑。
|// 假设我们有包含融资信息的公司数据 { "_id": "company123", "name": "创新科技", "ipo": { "publicYear": 2022, "valuationAmount": 10000000000, "currency": "CNY" }, "fundingRounds": [ { "investments": [ { "financialOrg": { "name": "红杉资本",
通过投影操作,我们可以将这些嵌套信息重新组织:
|db.companies.aggregate([ { $match: { "fundingRounds.investments.financialOrg.permalink": "sequoia-capital-china" } }, { $project: { _id: 0, companyName: "$name", ipoYear: "$ipo.publicYear", valuation: "$ipo.valuationAmount", investors: "$fundingRounds.investments.financialOrg.permalink" }} ])
在这个例子中,我们使用了字段路径表达式(以$开头的路径)来访问嵌套数据。$符号告诉MongoDB这是一个字段路径,应该从输入文档中提取相应的值。
投影操作能够处理复杂的数据结构转换。当我们处理包含数组的嵌套文档时,投影会自动处理数组中的每个元素。
|// 结果示例 { "companyName": "创新科技", "ipoYear": 2022, "valuation": 10000000000, "investors": [ ["sequoia-capital-china", "idg-capital"], ["tencent-investment", "alibaba-capital"] ] }
注意到investors字段变成了一个嵌套数组。这是因为fundingRounds本身是一个数组,而每个融资轮次的investments又是一个数组。MongoDB的投影操作会保持这种嵌套结构,生成对应的嵌套数组结果。
字段路径表达式不仅能够访问嵌套字段,还可以递归地穿透文档中多层嵌套的数组和对象,实现深层次的数据提取。例如,通过类似于$fundingRounds.investments.financialOrg.permalink这样的路径,我们可以一步到位地获取到嵌套在数组和嵌套对象中的具体值或子数组。
字段路径表达式还可结合聚合运算符配合使用,实现子字段的聚合、过滤、映射等高级功能,比如统计某一数组中所有嵌套对象的某个字段之和、最大值或筛选符合特定条件的子文档。
|// 更复杂的投影示例 db.companies.aggregate([ { $project: { _id: 0, basicInfo: { name: "$name", category: "$category", founded: "$foundedYear" }, financialSummary: { totalRounds: { $size: "$fundingRounds" }, lastFunding: { $arrayElemAt: ["$fundingRounds", -1] } } }} ])
在这个例子中,我们不仅使用了字段路径,还结合了数组操作表达式。arrayElemAt表达式提取数组中的特定元素。这种组合使用让我们能够创建全新的文档结构。
投影操作虽然强大,但不能改变字段值的数据类型。如果需要进行类型转换,需要结合其他表达式或在后续阶段进行处理。
投影操作不仅可以选择和重塑已有的字段,还支持创建计算字段。所谓计算字段,就是那些不直接来源于原始文档,而是通过表达式进行动态计算得出的新字段。 例如,你可以根据其他字段的运算结果、条件判断、字符串拼接、日期处理等多种场景,定义出有意义的辅助信息。这些计算字段可以用于展示、后续处理,或者支持更复杂的业务逻辑。 常见的做法包括:根据成立年份计算公司年龄,将多个字段拼接为完整地址,判断某个条件成立与否给出布尔标记,甚至可以结合多个表达式实现嵌套运算。
|// 创建计算字段 db.companies.aggregate([ { $project: { name: 1, foundedYear: 1, companyAge: { $subtract: [new Date().getFullYear(), "$foundedYear"] }, isStartup: { $lt: ["$foundedYear", 2020] } }} ])
这个例子创建了两个计算字段:companyAge通过当前年份减去成立年份计算公司年龄,isStartup通过比较成立年份判断是否为新创公司。
在处理包含数组字段的文档时,$unwind操作是一个极其重要的工具。它能够将数组字段「展开」,为数组中的每个元素创建一个独立的文档副本。这种操作让我们能够以文档级别来处理原本嵌套在数组中的数据。
想象一下,如果我们有一个包含多个融资轮次的公司文档,$unwind操作就像是把这个公司的故事分解成多个章节,每个章节专门讲述一轮融资的详细情况。
让我们通过一个具体例子来理解这个过程:
|// 原始文档结构 { "_id": "tech_startup", "name": "智能出行", "fundingRounds": [ { "round": "seed", "amount": 1000000, "year": 2020 }, { "round": "A", "amount": 5000000, "year": 2021 }, { "round":
使用$unwind操作后:
|db.companies.aggregate([ { $unwind: "$fundingRounds" } ]) // 结果: // 文档1: { "_id": "tech_startup", "name": "智能出行", "fundingRounds": { "round": "seed", "amount": 1000000, "year": 2020 } } // 文档2: { "_id": "tech_startup", "name": "智能出行", "fundingRounds": { "round": "A", "amount": 5000000, "year": 2021 } } // 文档3: { "_id": "tech_startup", "name": "智能出行", "fundingRounds": { "round": "B", "amount": 15000000, "year": 2022 } }
在没有使用$unwind之前,如果我们直接投影数组中的字段,会得到嵌套的数组结果,这往往不是我们想要的。
|// 不使用$unwind的投影结果 db.companies.aggregate([ { $project: { name: 1, amounts: "$fundingRounds.amount", years: "$fundingRounds.year" }} ]) // 结果: { "name": "智能出行", "amounts": [1000000, 5000000, 15000000], "years": [2020, 2021, 2022]
使用$unwind后,我们能够获得扁平化的结构:
|// 使用$unwind的完整管道 db.companies.aggregate([ { $unwind: "$fundingRounds" }, { $project: { _id: 0, name: 1, round: "$fundingRounds.round", amount: "$fundingRounds.amount", year: "$fundingRounds.year" }} ]) // 结果: { "name": "智能出行", "round": "seed", "amount": 1000000
$unwind操作会为原始数组中的每个元素创建一个完整的文档副本,只是数组字段被替换为单个元素值。这意味着如果数组有N个元素,$unwind会产生N个输出文档。
在实际应用中,我们经常需要结合使用多个$match阶段来实现精确筛选。第一个$match用于初步筛选,减少需要处理的文档数量;$unwind后的$match则用于精确筛选展开后的文档。
|// 多级筛选示例:查找红杉资本投资的轮次 db.companies.aggregate([ // 第一级筛选:找到有红杉资本投资的公司 { $match: { "fundingRounds.investors.name": "红杉资本中国" } }, // 展开融资轮次 { $unwind: "$fundingRounds" }, // 第二级筛选:只保留红杉资本参与的轮次 { $match: { "fundingRounds.investors.name": "红杉资本中国" } }, { $project: { _id: 0, companyName: "$name", investor: "$fundingRounds.investors.name", amount:
这种两阶段筛选策略既保证了查询效率,又确保了结果的准确性。第一个$match可以利用索引快速筛选相关文档,第二个$match则精确过滤展开后的结果。

MongoDB 的数组表达式为数据变换和复杂查询提供了高度灵活的数组处理能力,使我们能够在不对集合进行 unwinding 的情况下直接对数组进行筛选、投影与变换。这类操作在聚合管道的 $project 阶段极为常用,便于提取、重组及分析结构化和半结构化数据。
$filter 数组表达式可实现基于复杂条件的数组元素筛选,仅保留满足特定业务规则的数组成分。相比普通投影,$filter 能在数组结构保持不变的前提下,高效完成高粒度的数据过滤和数据质量管理。
|// 筛选大额融资轮次(超过1000万) db.companies.aggregate([ { $project: { name: 1, majorRounds: { $filter: { input: "$fundingRounds", as: "round", cond: { $gte: ["$$round.amount", 10000000] } } } }} ])
在这个例子中,$filter表达式包含三个关键参数:input指定要筛选的数组,as定义了在条件中引用数组元素的变量名,cond则定义了筛选条件。双美元符号($$)用于引用在as中定义的变量,这是为了区别于普通的字段路径引用。
$arrayElemAt 表达式用于按照指定索引精确提取数组中的特定元素。当实际业务中需要高效访问数组的首尾(如取「第一轮」或「最后一轮」融资信息)或任意指定位置的数据时,该表达式可显著提升数据处理的明确性与可读性。
|// 获取第一轮和最后一轮融资信息 db.companies.aggregate([ { $project: { _id: 0, name: 1, foundedYear: 1, firstRound: { $arrayElemAt: ["$fundingRounds", 0] }, lastRound: { $arrayElemAt: ["$fundingRounds", -1] } }} ])
数组索引从0开始,-1代表最后一个元素,-2代表倒数第二个元素,以此类推。这种索引方式让我们能够灵活地访问数组的不同位置。
$slice 表达式用于对数组进行片段化处理,从现有数组中高效提取所需的子数组。在面对大规模数据集时,$slice 能满足如限制输出前 N 项、或跳过部分元素后获取限定范围内容等常见的业务需求,提升数据访问的灵活性与性能。
|// 获取前3轮融资和跳过第一轮后的2轮融资 db.companies.aggregate([ { $project: { name: 1, earlyRounds: { $slice: ["$fundingRounds", 3] }, // 前3个元素 middleRounds: { $slice: ["$fundingRounds", 1, 2] } // 从索引1开始的2个元素 }} ])
$slice表达式可以接受两个或三个参数。两个参数时表示从头开始取指定数量的元素,三个参数时第二个参数是起始位置,第三个参数是要取的元素数量。
$size 表达式用于专业地计算数组的元素数量,是实现数组统计分析的基础工具。
|// 计算融资统计信息 db.companies.aggregate([ { $project: { name: 1, totalRounds: { $size: "$fundingRounds" }, hasIPO: { $ifNull: ["$ipo", false] }, fundingIntensity: { $cond: { if: { $gt: [{ $size: "$fundingRounds" }, 0] }, then: { $divide: [{ $sum: "$fundingRounds.amount" }, { $size: "$fundingRounds" }] }, else: 0 } } }} ])
在这个复杂的例子中,我们不仅计算了融资轮次的数量,还创建了一个「融资强度」指标,通过平均融资金额来衡量公司的融资能力。
数组表达式的强大之处在于它们可以组合使用。通过将多个数组操作组合在一起,我们能够实现非常复杂的数据分析和转换逻辑。
累加器是MongoDB聚合框架中的一种专业表达式,其核心能力在于对多条文档数据进行累计处理并生成专业的统计汇总结果。

累加器在管道处理阶段内部维持专用的累积状态。随着文档流经包含累加器的阶段,每遇到一条相关文档,累加器就根据指定逻辑持续更新其内部状态。待所有文档处理完毕后,累加器输出最终的统计值。 以班级平均分统计为例:累加器会专业地累加所有学生的分数并保持总数统计,最终通过人数进行运算,输出专属的平均分结果。
MongoDB提供了丰富的累加器类型,每种都有特定的应用场景。数值统计类累加器包括$sum(求和)、$avg(平均值)、$max(最大值)、$min(最小值),这些是最常用的统计工具。
数组构建类累加器如$push(追加元素)和$addToSet(添加唯一元素)则用于在聚合过程中动态构建数组。位置选择类累加器$first(第一个值)和$last(最后一个值)帮助我们从文档流中选择特定位置的值。
高级统计类累加器提供了更复杂的统计功能,比如$stdDevPop(总体标准差)和$stdDevSamp(样本标准差),这些在数据分析中非常有用。
从MongoDB 3.2开始,部分累加器可以在投影阶段使用。这些累加器与分组阶段中的同名操作不同,它们专门处理单个文档中的数组字段。
|// 在投影阶段使用累加器处理数组 db.companies.aggregate([ { $match: { "fundingRounds": { $exists: true, $ne: [] } } }, { $project: { _id: 0, name: 1, maxFunding: { $max: "$fundingRounds.amount" }, totalFunding: { $sum: "$fundingRounds.amount" }, avgFunding: { $avg: "$fundingRounds.amount" }, roundsCount: { $size: "$fundingRounds" } }} ])
这个例子展示了如何使用累加器来分析单个公司的融资数据。sum计算总融资额,$avg计算平均单轮融资额。注意这里的累加器是在每个文档内部进行计算,而不是跨文档统计。
投影阶段的累加器只能处理单个文档中的数组字段,而分组阶段的累加器则能够处理多个文档的数据流。
分组操作是数据分析领域中的核心技术之一,它用于将满足特定条件或共享某些特征的数据对象归纳为不同的数据集(分组),从而实现分群统计与分析。 在MongoDB的聚合框架中,$group阶段承担了这一功能,其原理与关系型数据库中的SQL GROUP BY子句高度类比,均实现基于键值的数据归集及高级聚合计算。

分组的本质在于定义“同组标准”,即明确用于分组的键或表达式。当文档流经过$group阶段时,系统依据指定的分组键(或表达式)对所有输入文档进行划分,值相同的文档自动归为同一组。针对每一分组,MongoDB会应用所指定的累加器运算(如sum、avg、max等),从而生成面向该组的聚合统计结果。
|// 按成立年份统计公司平均员工数 db.companies.aggregate([ { $group: { _id: { foundedYear: "$foundedYear" }, avgEmployees: { $avg: "$employees" }, companyCount: { $sum: 1 } }}, { $sort: { "avgEmployees": -1 } } ])
在这个例子中,所有在同一年成立的公司被分为一组,然后计算每组的平均员工数和公司数量。$sum: 1是一个常见的计数技巧,每处理一个文档就在计数器上加1。
$group阶段中的_id字段是分组操作的核心,它定义了文档的分组标准。_id字段的值决定了哪些文档会被归为同一组。设计良好的_id字段不仅能确保正确的分组逻辑,还能让结果更易于理解。
|// 单字段分组示例 db.companies.aggregate([ { $group: { _id: { category: "$category" }, // 按业务类别分组 companies: { $push: "$name" }, // 收集每个类别的公司名称 totalFunding: { $sum: { $sum: "$fundingRounds.amount" } } }} ])
为了让分组结果更清晰,我们通常将_id设置为文档形式,并给字段起有意义的名字。这样做的好处是输出结果中的_id字段会明确显示分组的依据。
在更复杂的分析场景中,我们可能需要根据多个字段的组合来进行分组。这时可以将多个字段组合在_id文档中。
|// 按成立年份和业务类别组合分组 db.companies.aggregate([ { $match: { foundedYear: { $gte: 2020 } } }, { $group: { _id: { year: "$foundedYear", category: "$category" }, companies: { $push: "$name" }, avgEmployees: { $avg: "$employees" } }}, { $sort: { "_id.year": 1, "_id.category": 1 } } ])
这种组合分组让我们能够进行更细粒度的数据分析,比如「每年每个行业的平均公司规模」这样的统计需求。
在group阶段,_id字段不仅可以设置为简单字段,还可以通过字段路径(如嵌套.字段名)来引用嵌套文档中的值,实现分组。
例如,如果文档结构中包含子文档,可以直接通过$_id: "$parent.child"来按子字段分组。这样,我们无需对原始文档结构进行展平,就能灵活地对复杂结构中的任意层级字段进行分组统计,对于如公司IPO信息、订单条目等嵌套数据分析场景尤为实用。
|// 按IPO年份分组 db.companies.aggregate([ { $match: { "ipo.publicYear": { $exists: true } } }, { $group: { _id: { ipoYear: "$ipo.publicYear" }, companies: { $push: "$name" }, avgValuation: { $avg: "$ipo.valuationAmount" } }}, { $sort: { "_id.ipoYear": 1 } } ])
这个例子按公司上市年份进行分组,计算每年上市公司的数量和平均估值。通过字段路径$ipo.publicYear,我们可以直接访问嵌套文档中的字段作为分组依据。
在设计分组字段时,要考虑数据的分布情况。如果某个字段的取值过于分散,可能会产生很多只包含少量文档的小组,这通常不是我们想要的分析结果。
在进行分组操作时,通常我们希望不仅仅是简单地将文档数量相加,还希望将分布在多个文档中的相关信息整合起来,形成更具业务意义的数据结构。例如,我们可能希望按公司合并所有融资事件、聚合用户的订单详情、统计某组中的首尾记录等。为了支持这样的需求,MongoDB 提供了多种累加器(Accumulator),其中最常用的包括:
$push:可以把每个分组中的值(可以是字段、对象或表达式结果)收集到一个数组里,从而实现“按组收集明细列表”的效果。例如,将每家公司的所有融资事件整合为一个数组。$first 和 $last:分别返回每组排序后第一个/最后一个文档指定字段的值。常用于获取每组的起始或结束状态,适合时间序列分析,比如找出用户的首笔和末笔订单、公司第一轮和最后一轮融资。$avg(计算平均值)、$sum(求和)、$min/$max`(求最小/最大),也可以与这些位置型累加器结合使用,丰富结果结构。通过这些累加器,我们可以灵活地重组和聚合原始分散的数据,实现类似“将每家公司融资历史按时间聚合成数组”“提取每个用户的首末交易”等复杂需求,而无需手动编码处理文档流,极大地提升了数据分析与报表的便利性和表达能力。
|// 构建按时间排序的融资历史 db.companies.aggregate([ { $match: { fundingRounds: { $ne: [] } } }, { $unwind: "$fundingRounds" }, { $sort: { "fundingRounds.fundedYear": 1, "fundingRounds.fundedMonth": 1, "fundingRounds.fundedDay": 1 }}, { $group: { _id: { company: "$name" }, fundingHistory: { $push: { round: "$fundingRounds.roundCode", amount: "$fundingRounds.raisedAmount",
这个管道首先展开融资轮次,然后按时间排序,最后将每个公司的融资轮次重新组合成一个按时间顺序排列的数组。$push累加器在这里起到了关键作用——它将每个融资轮次的信息打包成新的文档结构并添加到数组中。
first 和 last 累加器的作用是:在对数据进行分组并排序之后,分别从每个分组的文档流中提取第一个和最后一个文档的指定字段值。以时间序列数据为例,常常会先对各组内的记录按照时间字段升序或降序排列,然后用 first 获得时间最早的元素特征(如起始融资轮信息),用 last 获得时间最晚的元素特征(如最新融资轮信息)。 通过这种方式,能够很方便地对业务对象的首尾状态或区间跨度进行比较和分析。例如:可以直接分析每个公司初创时的融资金额与最新一轮融资金额之间的变化,或者从每个用户的第一笔和最近一笔交易提取行为轨迹,为增长分析或用户分层提供数据支撑。
|// 分析公司融资的首末轮对比 db.companies.aggregate([ { $match: { fundingRounds: { $exists: true, $ne: [] } } }, { $unwind: "$fundingRounds" }, { $sort: { "_id": 1, // 确保同一公司的记录聚集在一起 "fundingRounds.fundedYear": 1, "fundingRounds.fundedMonth": 1, "fundingRounds.fundedDay": 1 }}, { $group: { _id: { company: "$name" }, firstRound: { $first: "$fundingRounds" },
这个复杂的管道展示了多个高级技巧的组合使用。通过$first和$last累加器,我们能够同时获得每个公司的第一轮和最后一轮融资信息。然后在投影阶段,我们计算了融资规模的增长倍数,为后续分析提供了有价值的指标。
$first和$last累加器的行为依赖于文档进入分组阶段的顺序。因此在使用这些累加器之前,通常需要先进行排序操作以确保结果的可预测性。
分组操作和投影操作虽然都能使用累加器,但它们的工作方式和应用场景有着根本性的不同:
这种区别决定了某些累加器只能在特定的阶段使用。比如$push累加器只能在分组阶段使用,因为它需要收集来自多个文档的值来构建数组。
在高级数据分析流程结束后,通常需要将聚合结果持久化,以支持后续的数据复用与高性能访问。MongoDB 支持通过两种方式将聚合管道的输出写入集合:即 $out 与 $merge 操作符。

$out 用于将聚合管道的输出结果直接写入指定集合。若目标集合已存在,则会被新结果完全替换,实现数据的原子性覆盖。
|// 创建公司统计汇总表 db.companies.aggregate([ { $group: { _id: { category: "$category", foundedYear: "$foundedYear" }, companyCount: { $sum: 1 }, avgEmployees: { $avg: "$employees" }, totalFunding: { $sum: { $sum: "$fundingRounds.raisedAmount" } } }}, { $sort: { "_id.foundedYear": 1, "_id.category": 1 } }, { $out: "company_statistics" } // 输出到新集合
使用$out操作后,聚合结果会被保存到名为「company_statistics」的集合中,我们可以像使用普通集合一样查询和操作这些数据。
$out操作会完全替换目标集合的内容。如果目标集合中有重要数据,请务必先进行备份。另外,$out只能写入同一数据库中的集合。
$merge操作提供了更加灵活的输出选项,它可以将结果合并到现有集合中,而不是简单地覆盖。$merge支持多种合并策略,包括插入新文档、更新现有文档、或者在冲突时保持原有数据。
|// 增量更新公司统计数据 db.companies.aggregate([ { $match: { lastModified: { $gte: new Date("2024-01-01") } } }, { $group: { _id: "$_id", name: { $first: "$name" }, latestFunding: { $max: "$fundingRounds.raisedAmount" }, lastUpdated: { $max: "$lastModified" } }}, { $merge: { into: "company_snapshots", whenMatched: "merge", // 存在时合并字段 whenNotMatched: "insert" // 不存在时插入新文档
merge操作的强大之处在于它支持跨数据库操作,并且提供了丰富的合并选项。这使得它特别适合用于构建增量更新的数据管道和实时统计系统。
通过合理使用$merge操作,我们可以高效实现实时更新的数据统计视图。具体来说,聚合管道中的$group阶段首先对原始公司数据进行分组、汇总(如按行业、成立年份统计公司数量、平均员工数等),随后利用$merge阶段将聚合结果输出到指定集合。
与传统$out操作不同,$merge不仅支持将结果写入同数据库,还支持跨数据库写入,并能灵活选择是替换、合并还是插入数据,从而避免全量覆盖,适合实时或增量数据更新场景。这
种机制允许我们定期或按事件触发执行聚合任务,将最新统计结果写入视图集合,供业务应用高效查询,无需每次实时计算复杂统计逻辑,大幅提升读取性能和应用响应速度。 例如,可以通过定时任务周期性运行上述聚合管道,不断刷新“行业统计”“公司快照”等视图集合,让应用层可直接获取最新的结构化统计数据,实现准实时分析与决策。
|// 创建实时更新的行业统计视图 db.companies.aggregate([ { $group: { _id: "$category", totalCompanies: { $sum: 1 }, avgFoundedYear: { $avg: "$foundedYear" }, totalEmployees: { $sum: "$employees" }, activeCompanies: { $sum: { $cond: [{ $eq: ["$status", "active"] }, 1, 0] } }, lastUpdated: { $max: "$$NOW" } }}, { $merge: { into: { db: "analytics"
假设我们有一个学生成绩集合 students,其中的文档结构如下:
|[ { "_id": "stu001", "name": "张三", "age": 20, "major": "计算机科学", "scores": { "math": 85, "english": 92, "programming": 88 } }, { "_id": "stu002",
请编写聚合管道,筛选出计算机科学专业且年龄大于19岁的学生,并只显示他们的姓名和数学成绩。
|// 答案 db.students.aggregate([ { $match: { major: "计算机科学", age: { $gt: 19 } }}, { $project: { _id: 0, name: 1, mathScore: "$scores.math" }} ]) // 结果: // { "name": "张三", "mathScore": 85 } // { "name": "王五", "mathScore": 95 }
使用以下的订单数据集合 orders:
|[ { "_id": "order001", "customerName": "陈先生", "orderDate": "2024-01-15", "items": [ { "product": "笔记本电脑", "price": 8000, "quantity": 1 }, { "product": "鼠标", "price": 150, "quantity": 2
请编写聚合管道,展开订单中的商品项目,并显示每个商品的详细信息(客户姓名、商品名称、单价、数量)。
|// 答案 db.orders.aggregate([ { $unwind: "$items" }, { $project: { _id: 0, customerName: 1, product: "$items.product", price: "$items.price", quantity: "$items.quantity" }} ]) // 结果: // { "customerName": "陈先生", "product": "笔记本电脑", "price": 8000, "quantity": 1 } // { "customerName": "陈先生", "product": "鼠标", "price": 150, "quantity": 2 } // { "customerName": "林女士", "product": "键盘", "price": 300, "quantity": 1 } // { "customerName": "林女士", "product": "显示器", "price": 1200, "quantity": 1 }
使用以下的销售记录集合 sales:
|[ { "_id": "sale001", "product": "iPhone 15", "category": "手机", "price": 6999, "quantity": 2, "salesperson": "小明" }, { "_id": "sale002", "product": "MacBook Pro", "category": "电脑"
请按销售人员分组,计算每个销售人员的总销售额(单价 × 数量之和)和销售的产品数量。
|// 答案 db.sales.aggregate([ { $group: { _id: "$salesperson", totalSales: { $sum: { $multiply: ["$price", "$quantity"] } }, productCount: { $sum: 1 } }}, { $project: { _id: 0, salesperson: "$_id", totalSales: 1, productCount: 1 }} ]) // 结果:
使用以下的员工数据集合 employees:
|[ { "_id": "emp001", "name": "赵经理", "department": "技术部", "salary": 15000, "projects": [ { "name": "项目A", "status": "completed", "hours": 120 }, { "name": "项目B", "status":
请筛选出已完成的项目,并计算每个员工已完成项目的总工时和项目数量。
|// 答案 db.employees.aggregate([ { $project: { name: 1, department: 1, completedProjects: { $filter: { input: "$projects", as: "project", cond: { $eq: ["$$project.status", "completed"] } } } }}, { $project: { name: 1, department: 1, totalHours: { $sum:
使用以下的图书数据集合 books:
|[ { "_id": "book001", "title": "JavaScript高级程序设计", "author": "Nicholas C. Zakas", "price": 89, "pages": 600, "rating": 4.5, "reviews": [ { "user": "user1", "rating": 5, "date": "2024-01-01"
请创建新的字段:每页价格(price/pages)、平均读者评分(基于reviews数组计算)、以及书籍是否为畅销书(平均评分 >= 4.5)。
|// 答案 db.books.aggregate([ { $project: { title: 1, author: 1, price: 1, pages: 1, pricePerPage: { $divide: ["$price", "$pages"] }, avgReaderRating: { $avg: "$reviews.rating" }, isBestseller: { $gte: [{ $avg: "$reviews.rating" }, 4.5] }, reviewCount: { $size: "$reviews" } }}