聚合操作基础
MongoDB聚合管道是数据分析的核心工具,通过多阶段处理实现复杂的数据转换和计算。其基础结构由以下要素构成:
1 ) aggregate()函数
pipeline(必选):定义有序处理阶段(如$match,$project),每个阶段输出作为下一阶段输入options(可选): 配置聚合操作的附加参数(如读写关注、游标批处理大小)db.collection.aggregate([...], { allowDiskUse: true, // 允许大数据集使用磁盘缓存 maxTimeMS: 30000 // 超时控制(毫秒) })
代码示例:函数基本结构
// MongoDB Shell
db.collection.aggregate([
{ $stage1: { ... } },
{ $stage2: { ... } }
], {
allowDiskUse: true, // 允许使用磁盘缓存大数据集
maxTimeMS: 30000 // 设置超时时间
})
2 )执行特性
- 游标机制:返回可迭代游标,Shell默认显示前20条结果。
- 非破坏性操作:不影响原始集合数据。
表达式详解
表达式是聚合操作的"计算单元",分为三类:
| 类型 | 语法 | 作用 | 示例 |
|---|---|---|---|
| 字段路径表达式 | $字段名 | 定位文档字段 | $balance(顶级字段) |
$内嵌文档.字段 | 定位嵌套字段 | $name.first | |
| 系统变量表达式 | $$变量名 | 引用系统变量 | $$CURRENT(当前文档,类似this) |
| 常量表达式 | { $literal: 值 } | 避免$开头的字符串被误解析为字段 | { $literal: "$name" } → 输出字符串"$name" |
-
字段路径表达式
- 语法:
$字段名或$内嵌文档.字段名 - 作用:指向文档中的特定字段。
- 语法:
$name→ 顶级字段$info.dateOpened→ 内嵌文档字段
- 示例:
- 顶级字段:
$balance(指向余额字段) - 内嵌字段:
$name.first(指向name文档中的first字段)
- 顶级字段:
- 语法:
-
系统变量表达式
- 语法:
$$变量名 - 核心变量:
$$CURRENT:指代管道中当前操作的文档(类似 OOP 中的this)。- 等价关系:
$balance≡$$CURRENT.balance
- 语法:
-
常量表达式
- 语法:
{ $literal: 值 } - 场景:避免以
$开头的字符串被误解析为字段路径。 - 示例:
{ $project: { rawString: { $literal: "$name" } // 输出字符串 "$name" 而非字段值 }}
- 语法:
要点
- 表达式优先级:
$前缀> 系统变量 > 常量(需显式声明)。 - 缺失字段处理:自动填充
null值(如$name.middle→null)。
核心聚合阶段
以下操作基于银行账户集合 accounts,文档结构示例:
{
_id: ObjectId("..."),
name: { first: "Alice", last: "Smith" },
balance: 50,
currency: ["CNY", "USD"] // 部分文档为字符串(如"GBP")
}
按执行优先级排序,需组合使用以优化性能:
1 ) $match:文档筛选
-
功能:过滤文档,语法与查询操作符完全一致。
-
优化原则:尽可能在管道前端使用,减少后续处理的数据量。
-
示例1:精确匹配名字
db.accounts.aggregate([ { $match: { "name.first": "Alice" } } // 仅保留Alice的文档 ]); -
示例2:复合条件(
$or与范围查询)db.accounts.aggregate([ { $match: { $or: [ { balance: { $gt: 40, $lt: 80 } }, // 余额40~80 { "name.last": "Yang" } // 姓氏为Yang ] } } ]);
综合示例(结合 $project):
db.accounts.aggregate([
{
$match: {
$or: [
{ balance: { $gt: 40, $lt: 80 } }, // // 余额40~80
{ "name.last": "杨" } // 姓氏为 杨
]
}
},
{
$project: { _id: 0, balance: 1, lastName: "$name.last" }
}
])
优化建议:在管道起始阶段使用$match减少后续处理量
2 ) $project:字段重塑
- 功能:控制输出字段、添加计算字段或剔除字段
- 语法规则:
字段: 1→ 保留字段字段: 0→ 排除字段新字段: 表达式→ 动态生成字段
- 实战演示:
db.accounts.aggregate([ { $project: { _id: 0, // 排除文档ID balance: 1, // 保留余额 clientName: "$name.first", // 从内嵌文档提取值 nameArray: [ // 构建数组字段 "$name.first", "$name.middle", // 指向不存在的字段 → 返回 null "$name.last" ] } } ])
输出效果:
-
原字段
name被替换为clientName和nameArray -
缺失字段(如
middle)自动填充null -
示例2:隐藏
_id、保留balance、新增client_namedb.accounts.aggregate([ { $project: { _id: 0, // 隐藏_id balance: 1, // 保留balance client_name: "$name.first" // 新增字段(从name.first提取) } } ]);输出:
{ "balance": 50, "client_name": "Alice" } -
示例3:处理缺失字段
db.accounts.aggregate([ { $project: { name_array: [ "$name.first", "$name.middle", // 原文档无此字段 → 返回null "$name.last" ] } } ]);输出:
{ "name_array": ["Alice", null, "Smith"] }
注意:
aggregate()返回文档游标,不影响原集合数据$project不修改原集合,仅影响管道中的文档形态
3 ) $unwind:数组展开
-
功能:将数组拆分为多篇独立文档(每元素一篇)
-
参数逻辑对照表:
参数 作用 输出影响(空数组场景) path: "$数组字段"指定目标字段 默认丢弃 includeArrayIndex: "下标名"记录元素原索引位置 非数组字段→ 下标名: nullpreserveNullAndEmptyArrays: true保留空数组/null文档 保留原文档 -
特殊处理逻辑:
- 非数组字段 → 视为单元素数组处理
- 空数组/null字段 → 默认丢弃(需开启
preserveNullAndEmptyArrays保留)
-
基础语法:
{ $unwind: { path: "$数组字段" } } -
示例1:展开数组字段
db.accounts.aggregate([{ $unwind: "$currency" }]);输入文档:
{ _id: 1, currency: ["CNY", "USD"] } // Alice { _id: 2, currency: "GBP" } // Bob输出:
{ _id: 1, currency: "CNY" }, // 原数组拆为两篇 { _id: 1, currency: "USD" }, { _id: 2, currency: "GBP" } // 非数组字段原样保留 -
进阶参数:
{ $unwind: { path: "$currency", includeArrayIndex: "currIndex", // 添加元素原下标 preserveNullAndEmptyArrays: true // 保留空数组/null值文档 } }输出效果:
- 数组元素:
currIndex记录下标(从0开始) - 非数组字段:
currIndex: null - 空数组/字段缺失:保留文档(需开启
preserveNullAndEmptyArrays)
- 数组元素:
综合示例:
// 展开 currency 数组并保留索引
db.accounts.aggregate([
{
$unwind: {
path: "$currency",
includeArrayIndex: "currIndex",
preserveNullAndEmptyArrays: true
}
}
])
4 )$limit 与 $skip :结果集分页
$limit:保留前 N 篇文档({ $limit: N })$skip:跳过前 M 篇文档({ $skip: M })- 典型场景:分页查询(需配合
$sort确保顺序一致) - 分页组合:
[{ $skip: 10 }, { $limit: 5 }] // 跳过前10条,取5条(需配合$sort保证顺序)
5 ) $sort :排序
- 语法:
{ $sort: { 字段1: 排序方向, 字段2: 排序方向 } }1→ 升序-1→ 降序
- 用例:
db.accounts.aggregate([ { $sort: { balance: 1, // 余额升序 "name.last": -1 // 姓氏降序 } } ]);
要点
- 阶段顺序黄金法则:
$match→$project→$unwind→$sort→$limit/$skip - 内存警告:
$unwind可能大幅增加文档量,需搭配allowDiskUse: true
聚合管道设计原则与性能优化
1 ) 阶段顺序重要性:
- 优先执行
$match和$project减少数据处理量 $unwind可能导致文档数量激增,需谨慎放置
2 ) 游标机制:
- 聚合结果以游标形式返回,支持分批处理大数据集
- Shell 中默认打印前 20 篇文档
3 ) 资源控制:
- 通过
options参数设置allowDiskUse: true避免内存溢出
组合管道实战
场景:银行账户数据分析(集合:accounts)
// 目标:筛选高价值客户,提取姓名+余额,按货币拆分后排序
db.accounts.aggregate([
{ $match: { balance: { $gt: 100 } }}, // 1. 过滤余额>100
{ $project: { // 2. 重塑结构
_id: 0,
fullName: { $concat: ["$name.first", " ", "$name.last"] },
balance: 1,
currency: 1
}},
{ $unwind: { // 3. 拆分货币数组
path: "$currency",
preserveNullAndEmptyArrays: false
}},
{ $sort: { currency: 1, balance: -1 } } // 4. 按货币升序+余额降序
])
输入文档示例:
{
_id: ObjectId("60a1b2c3d4e5f6a7b8c9d0e1"),
name: { first: "张", last: "伟" },
balance: 150,
currency: ["CNY", "USD"]
}
输出结果:
{ "fullName": "张 伟", "balance": 150, "currency": "CNY" }
{ "fullName": "张 伟", "balance": 150, "currency": "USD" }
NestJS集成示例
1 ) 示例1:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { AccountDocument } from './account.schema';
@Injectable()
export class AccountService {
constructor(
@InjectModel('Account') private accountModel: Model<AccountDocument>,
) {}
// 聚合查询:获取姓氏为"Yang"的客户全名与余额(降序)
async getHighValueClients(): Promise<any[]> {
const pipeline = [
{ $match: { "name.last": "Yang" } },
{ $project: {
_id: 0,
fullName: { $concat: ["$name.first", " ", "$name.last"] },
balance: 1
}},
{ $sort: { balance: -1 } } // 余额倒序排列
];
return this.accountModel.aggregate(pipeline).exec();
}
}
2 ) 示例2:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { AccountDocument } from './account.schema';
@Injectable()
export class AccountService {
constructor(
@InjectModel('Account') private accountModel: Model<AccountDocument>,
) {}
async aggregateAccounts() {
return this.accountModel.aggregate([
{ $match: { 'name.first': 'Alice' } },
{ $project: { _id: 0, balance: 1, clientName: '$name.first' } }
]).exec();
}
}
3 ) 示例3:
// NestJS 中使用聚合管道
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { AccountDocument } from './account.schema';
@Injectable()
export class AccountService {
constructor(
@InjectModel('Account') private accountModel: Model<AccountDocument>,
) {}
async runAggregationPipeline() {
return this.accountModel.aggregate([
{ $match: { 'name.lastName': 'Yang' } },
{
$project: {
_id: 0,
fullName: { $concat: ["$name.firstName", " ", "$name.lastName"] },
balance: 1
}
},
{ $sort: { balance: -1 } }
]).exec();
}
}
关键点总结:
- 字段路径表达式(
$field)是聚合操作的基础定位工具 $project重塑文档时支持动态字段生成与复杂嵌套提取$unwind数组展开需警惕空值处理逻辑,通过preserveNullAndEmptyArrays保留异常文档- 管道阶段顺序直接影响性能,优先过滤(
$match)和精简字段($project)
原生 SQL 对比参考(非 MongoDB 语法):
/* 类似 $project + $match 效果的 SQL */
SELECT first_name AS clientName, balance
FROM accounts
WHERE balance > 40
ORDER BY clientName ASC;
最佳实践与注意事项
1 ) 性能优化关键:
- 前置过滤:尽早使用
$match减少文档量。 - 字段精简:
$project剔除无用字段,降低内存占用。 - 警惕
$unwind+$group组合:大数据集可能触发内存溢出。
2 ) 特殊场景处理:
- 空数组/缺失字段:
$unwind默认丢弃,需显式启用preserveNullAndEmptyArrays: true保留。 - 非常量字符串:以
$开头的文本必须用$literal包裹(如{ $literal: "$test" })。
3 ) 错误规避策略:
// 错误示例:未包裹的$开头的字符串
{ $project: { raw: "$name" } } // 解析为字段 → 输出"Zhang"
// 正确修复:使用$literal
{ $project: { raw: { $literal: "$name" } } } // 输出字符串"$name"
要点
- 聚合管道 = 数据工厂流水线:阶段顺序决定逻辑正确性与性能。
- 表达式是基石:精准区分字段路径(
$field)、系统变量($$CURRENT)、常量($literal)。 - 终极口诀:先过滤、再精简、慎拆数组、排序分页收尾。
核心价值总结
- 聚合表达式是指定字段操作的基础,区分
$字段(路径)、$$变量(系统)、$literal(常量) - 管道阶段需有序组合:
$match→$project→$unwind→$sort→$limit/$skip是高效执行的关键路径 $unwind的特殊性:默认丢弃空数组/null文档,需显式启用preserveNullAndEmptyArrays保留- 性能要点:尽早过滤数据、避免中间阶段产生过大临时结果集
- 游标特性:
aggregate()返回游标,不修改原集合数据。- 未赋值的游标在Mongo Shell中自动打印前20条结果。
- 性能优化:
- 尽早使用
$match、$project减少处理量 - 避免在
$unwind后接$group导致内存溢出(大数据集)
- 尽早使用
- 特殊处理:
$unwind默认丢弃空数组/null字段文档,需显式开启preserveNullAndEmptyArrays保留。
MongoDB聚合管道将多步数据处理压缩为单次操作,适用于:
- 实时分析:如金融账户风险评估
- 数据清洗:转换嵌套结构或无效值
- 报表生成:组合统计与排序输出
通过本文的表达式解析、阶段优化法则及实战案例,可高效解决复杂业务场景的数据处理需求
741



被折叠的 条评论
为什么被折叠?



