MongoDB 聚合框架实战:取代复杂 SQL 的文档操作

MongoDB 聚合框架实战:取代复杂 SQL 的文档操作
引言
MongoDB 聚合框架(Aggregation Framework) 是 MongoDB 的核心功能之一,它提供了一种强大的数据处理管道机制,可以对文档集合进行复杂的转换、过滤和计算操作。
对于习惯了关系型数据库的开发者来说,理解聚合框架的关键在于建立与 SQL JOIN 的对应关系。MongoDB 虽然没有传统意义上的 JOIN,但通过聚合管道中的 `$lookup` 等操作,可以实现类似甚至更灵活的关联查询。
聚合框架 vs SQL JOIN:
| SQL 操作 | MongoDB 聚合操作 | 说明 |
|---|---|---|
| SELECT | `$project` | 选择字段 |
| WHERE | `$match` | 过滤文档 |
| GROUP BY | `$group` | 分组聚合 |
| JOIN | `$lookup` | 文档关联 |
| ORDER BY | `$sort` | 排序 |
| LIMIT/OFFSET | `$limit`/$`skip` | 分页限制 |
| COUNT | `$group` + `_id: null` | 统计计数 |
本教程将带你掌握 MongoDB 聚合框架的实战技巧,从基础到高级,让你能够用聚合管道高效完成复杂的数据处理任务。
适用读者: 中级 MongoDB 开发者、后端工程师、数据分析师
—
基础聚合操作
1. $match – 数据过滤
`$match` 用于在聚合管道早期过滤文档,类似于 SQL 的 WHERE 子句。
“`javascript
// 查找所有年龄大于 25 的员工
db.employees.aggregate([
{ $match: { age: { $gt: 25 } } }
]);
// 复合条件过滤
db.employees.aggregate([
{
$match: {
department: ‘研发部’,
salary: { $gte: 10000, $lte: 20000 },
status: ‘active’
}
}
]);
// 使用正则表达式
db.employees.aggregate([
{ $match: { name: /^张/ } } // 名字以张开头
]);
性能提示: `$match` 应尽量放在管道开头,充分利用索引。
2. $project - 字段选择
`$project` 用于重新格式化输出文档,选择需要的字段,类似 SQL 的 SELECT 子句。
javascript
// 选择特定字段并添加计算字段
db.employees.aggregate([
{
$project: {
name: 1,
department: 1,
monthlySalary: { $divide: [‘$salary’, 12] }, // 计算月薪
fullYears: { $divide: [{ $toInt: ‘$age’ }, 12] } // 年龄转月份
}
}
]);
// 排除字段
db.employees.aggregate([
{ $project: { password: 0, ssn: 0 } }
]);
// 重命名字段
db.employees.aggregate([
{
$project: {
employeeName: ‘$name’,
annualCompensation: ‘$salary’
}
}
]);
3. $sort - 排序
javascript
// 按薪资降序排列
db.employees.aggregate([
{ $sort: { salary: -1 } }
]);
// 多字段排序
db.employees.aggregate([
{
$sort: {
department: 1, // 升序
salary: -1 // 降序
}
}
]);
// 按日期排序
db.orders.aggregate([
{ $sort: { createdAt: -1 } } // 最新订单在前
]);
4. $limit 和 $skip - 分页限制
javascript
// 获取前 10 条记录(LIMIT 10)
db.employees.aggregate([
{ $limit: 10 }
]);
// 分页(OFFSET 10, LIMIT 20)
db.employees.aggregate([
{ $skip: 10 },
{ $limit: 20 }
]);
// 带过滤的分页
db.orders.aggregate([
{ $match: { status: ‘completed’ } },
{ $skip: 100 },
{ $limit: 20 },
{ $sort: { createdAt: -1 } }
]);
---
分组聚合实战:按部门统计员工薪资
$group 核心概念
`$group` 用于将文档分组,并对每个组执行聚合操作,类似 SQL 的 GROUP BY。
基础分组示例
javascript
// 统计每个部门的员工数量
db.employees.aggregate([
{
$group: {
_id: ‘$department’, // 按部门分组
count: { $sum: 1 } // 计数
}
},
{ $sort: { count: -1 } }
]);
// 结果:
// { _id: “研发部”, count: 15 }
// { _id: “市场部”, count: 10 }
薪资统计实战
javascript
// 部门薪资详细统计
db.employees.aggregate([
{
$group: {
_id: ‘$department’,
totalEmployees: { $sum: 1 },
avgSalary: { $avg: ‘$salary’ },
minSalary: { $min: ‘$salary’ },
maxSalary: { $max: ‘$salary’ },
totalSalary: { $sum: ‘$salary’ },
salaryRange: { $subtract: [‘$max’, ‘$min’] } // 注意:这是错误的写法
}
},
{
$project: {
department: ‘$_id’,
totalEmployees: 1,
avgSalary: { $round: [‘$avgSalary’, 2] },
minSalary: 1,
maxSalary: 1,
salaryRange: {
$subtract: [
{ $max: ‘$maxSalary’ },
{ $min: ‘$minSalary’ }
]
},
totalSalary: 1,
_id: 0
}
},
{ $sort: { totalEmployees: -1 } }
]);
高级分组技巧
javascript
// 多字段分组:按部门 + 职位统计
db.employees.aggregate([
{
$group: {
_id: {
department: ‘$department’,
position: ‘$position’
},
count: { $sum: 1 },
avgSalary: { $avg: ‘$salary’ }
}
},
{
$project: {
department: ‘$_id.department’,
position: ‘$_id.position’,
employeeCount: ‘$count’,
avgSalary: 1,
_id: 0
}
}
]);
// 条件分组统计
db.employees.aggregate([
{
$group: {
_id: ‘$department’,
highPaidCount: {
$sum: { $cond: [{ $gte: [‘$salary’, 15000]}, 1, 0] }
},
lowPaidCount: {
$sum: { $cond: [{ $lt: [‘$salary’, 10000]}, 1, 0] }
},
totalCount: { $sum: 1 }
}
}
]);
// 嵌套分组统计
db.employees.aggregate([
{
$group: {
_id: null, // 全局分组
departments: {
$push: {
name: ‘$department’,
avgSalary: ‘$salary’,
count: { $sum: 1 }
}
},
companyTotalSalary: { $sum: ‘$salary’ },
companyAvgSalary: { $avg: ‘$salary’ }
}
}
]);
SQL 对比
sql
— SQL 写法
SELECT
department,
COUNT(*) as totalEmployees,
AVG(salary) as avgSalary,
MIN(salary) as minSalary,
MAX(salary) as maxSalary,
SUM(salary) as totalSalary
FROM employees
GROUP BY department
ORDER BY totalEmployees DESC;
---
管道连接实战:文档关联查询
$lookup - MongoDB 的 JOIN
`$lookup` 实现左外连接,类似于 SQL 的 LEFT JOIN。
基础关联查询
javascript
// 员工表 + 部门表关联
db.employees.aggregate([
{
$lookup: {
from: ‘departments’, // 关联的集合
localField: ‘department’, // 本地字段
foreignField: ‘name’, // 外部字段
as: ‘deptInfo’ // 结果字段名
}
},
{
$unwind: ‘$deptInfo’ // 展开数组
},
{
$project: {
name: 1,
salary: 1,
deptName: ‘$deptInfo.name’,
deptBudget: ‘$deptInfo.budget’,
_id: 0
}
}
]);
多重关联查询
javascript
// 员工 + 部门 + 项目多表关联
db.employees.aggregate([
// 关联部门
{
$lookup: {
from: ‘departments’,
localField: ‘department’,
foreignField: ‘name’,
as: ‘department’
}
},
// 关联项目
{
$lookup: {
from: ‘projects’,
localField: ‘projects’, // 员工集合中的项目 ID 数组
foreignField: ‘_id’,
as: ‘projectDetails’
}
},
{
$project: {
name: 1,
department: { $arrayElemAt: [‘$department’, 0] }, // 获取第一个元素
projects: ‘$projectDetails’,
salary: 1
}
}
]);
条件关联(Match 阶段)
javascript
// 只关联活跃的项目
db.employees.aggregate([
{
$lookup: {
from: ‘projects’,
localField: ‘projects’,
foreignField: ‘_id’,
as: ‘activeProjects’,
let: { empDept: ‘$department’ },
pipeline: [
{
$match: {
$expr: { $eq: [‘$status’, ‘active’] },
$expr: { $eq: [‘$department’, ‘$$empDept’] } // 同部门项目
}
}
]
}
}
]);
SQL 对比
sql
— SQL 左外连接
SELECT
e.name,
e.salary,
d.name as deptName,
d.budget as deptBudget
FROM employees e
LEFT JOIN departments d ON e.department = d.name
WHERE e.status = ‘active’;
---
数组操作实战
$unwind - 展开数组
javascript
// 展开技能数组
db.employees.aggregate([
{
$unwind: ‘$skills’ // 每个技能成为独立文档
},
{
$group: {
_id: ‘$skills’,
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } }
]);
// 保留原始数组
db.employees.aggregate([
{
$unwind: {
path: ‘$skills’,
preserveNullAndEmptyArrays: true // 保留空数组文档
}
}
]);
$push - 推入数组
javascript
// 按部门汇总技能
db.employees.aggregate([
{
$group: {
_id: ‘$department’,
allSkills: { $push: ‘$skills’ }, // 推入数组
employeeNames: { $push: ‘$name’ }
}
},
{
$unwind: ‘$allSkills’
},
{
$group: {
_id: ‘$_id’,
uniqueSkills: { $addToSet: ‘$allSkills’ }, // 去重
employeeNames: { $first: ‘$employeeNames’ }
}
}
]);
$addToSet - 去重推入
javascript
// 统计每个部门的技能类型(去重)
db.employees.aggregate([
{
$group: {
_id: ‘$department’,
skillsSet: { $addToSet: ‘$skills’ } // 自动去重
}
},
{
$project: {
department: ‘$_id’,
skillCount: { $size: ‘$skillsSet’ },
allSkills: ‘$skillsSet’,
_id: 0
}
}
]);
数组过滤与映射
javascript
// 过滤高薪员工
db.employees.aggregate([
{
$project: {
name: 1,
projects: {
$filter: {
input: ‘$projects’,
as: ‘proj’,
cond: { $gt: [‘$$proj.budget’, 100000] }
}
}
}
}
]);
// 数组映射
db.employees.aggregate([
{
$project: {
name: 1,
salaryHistory: {
$map: {
input: ‘$salaryHistory’,
as: ‘year’,
in: {
year: ‘$$year’,
bonus: { $multiply: [‘$$year’, 0.1] }
}
}
}
}
}
]);
---
高级计算实战
$addFields - 添加计算字段
javascript
// 添加多个计算字段
db.employees.aggregate([
{
$addFields: {
monthlySalary: { $divide: [‘$salary’, 12] },
taxAmount: { $multiply: [‘$monthlySalary’, 0.15] },
netSalary: { $subtract: [‘$monthlySalary’, ‘$taxAmount’] },
experienceYears: { $divide: [{ $subtract: [2026, ‘$hireYear’] }, 12] }
}
},
{
$project: {
name: 1,
monthlySalary: 1,
taxAmount: { $round: [‘$taxAmount’, 2] },
netSalary: { $round: [‘$netSalary’, 2] }
}
}
]);
$set - 简化字段设置
javascript
// $set 是 $addFields 的简化版本
db.employees.aggregate([
{
$set: {
departmentLevel: {
$switch: {
branches: [
{ case: { $eq: [‘$department’, ‘研发部’] }, then: ‘A’ },
{ case: { $eq: [‘$department’, ‘市场部’] }, then: ‘B’ },
{ case: { $eq: [‘$department’, ‘财务部’] }, then: ‘C’ }
],
default: ‘D’
}
}
}
}
]);
$mergeObjects - 合并对象
javascript
// 合并地址对象
db.employees.aggregate([
{
$addFields: {
address: {
$mergeObjects: [
‘$homeAddress’,
‘$workAddress’
]
}
}
}
]);
// 动态合并对象数组
db.employees.aggregate([
{
$addFields: {
allInfo: {
$reduce: {
input: [‘$personalInfo’, ‘$workInfo’],
initialValue: {},
in: { $mergeObjects: [‘$$value’, ‘$$this’] }
}
}
}
}
]);
数学计算实战
javascript
// 薪资分析计算
db.employees.aggregate([
{
$group: {
_id: ‘$department’,
employees: { $push: ‘$$ROOT’ }
}
},
{
$addFields: {
salaryStats: {
$avg: ‘$salary’,
min: { $min: ‘$salary’ },
max: { $max: ‘$salary’ },
percentile90: {
$arrayElemAt: [
{
$sortArray: {
input: ‘$salary’,
sortBy: 1
}
},
{ $ceil: { $multiply: [{ $size: ‘$salary’ }, 0.9] } }
]
}
}
}
}
]);
---
性能优化技巧
1. 索引优化
javascript
// 在 $match 之前创建索引
// 最佳实践:先 match,再其他操作
// 创建复合索引
db.employees.createIndex({ department: 1, salary: -1 });
// 管道优化示例
// ❌ 错误:先排序再过滤
db.employees.aggregate([
{ $sort: { salary: -1 } },
{ $match: { department: ‘研发部’ } }
]);
// ✅ 正确:先过滤再排序
db.employees.aggregate([
{ $match: { department: ‘研发部’ } },
{ $sort: { salary: -1 } }
]);
2. 管道顺序优化
javascript
// 优化建议:
// 1. $match 放在最前面(利用索引)
// 2. $project 尽早使用(减少数据传输)
// 3. $group 放在中间(减少数据量)
// 4. $sort/$limit 放在最后(只排序结果)
db.employees.aggregate([
{ $match: { status: ‘active’ } }, // ① 先用索引过滤
{ $project: { name: 1, salary: 1 } }, // ② 减少字段
{ $group: { _id: ‘$department’, avgSalary: { $avg: ‘$salary’ } } },
{ $sort: { avgSalary: -1 } }, // ③ 最后排序
{ $limit: 10 } // ④ 最后限制
]);
3. 避免全表扫描
javascript
// ❌ 避免:在 $group 后使用 $match
db.employees.aggregate([
{ $group: { _id: ‘$department’, count: { $sum: 1 } } },
{ $match: { count: { $gt: 10 } } } // 会处理所有分组
]);
// ✅ 优化:在 $group 前过滤
db.employees.aggregate([
{ $match: { status: ‘active’ } }, // 先过滤
{ $group: { _id: ‘$department’, count: { $sum: 1 } } },
{ $match: { count: { $gt: 10 } } } // 只过滤需要的分组
]);
4. 使用 $redact 条件过滤
javascript
// 使用 $redact 在聚合过程中过滤文档
db.employees.aggregate([
{
$redact: {
$cond: {
if: { $gte: [‘$salary’, 10000] },
then: ‘$$DESCEND’,
else: ‘$$PRUNE’
}
}
}
]);
5. 批量聚合
javascript
// 使用 allowDiskUse 处理大数据集
db.employees.aggregate([
{ $match: { status: ‘active’ } },
{ $group: { _id: ‘$department’, count: { $sum: 1 } } }
], { allowDiskUse: true });
// 使用 batchSize 控制内存
const cursor = db.employees.aggregate(pipeline, {
batchSize: 1000,
maxTimeMS: 30000
});
---
总结与最佳实践
核心要点回顾
- 聚合框架优势:强大的数据转换能力,无需应用层处理
- $match 优化:尽早使用,充分利用索引
- $group 灵活:支持多维度分组和聚合计算
- $lookup 强大:实现文档关联,类似 SQL JOIN
- 数组操作:$unwind、$push、$addToSet 处理嵌套数据
- 性能优先:合理的管道顺序至关重要
最佳实践清单
✅ 设计原则
- 将 `$match` 放在管道开头
- 使用 `$project` 尽早减少字段
- 避免在管道中间进行全表扫描
- 使用索引配合聚合查询
✅ 性能优化
- 在 `from` 字段上创建索引
- 使用 `allowDiskUse` 处理大数据
- 控制 `batchSize` 避免内存溢出
- 监控聚合执行计划(`explain()`)
✅ 代码规范
- 为每个管道步骤添加注释
- 保持管道步骤清晰独立
- 使用变量存储中间结果
- 避免过深的管道嵌套
✅ 调试技巧
- 使用 `explain()` 分析执行计划
- 逐步构建管道测试每个阶段
- 使用 `$debug` 步骤查看中间结果
- 监控数据库性能指标
SQL vs MongoDB 对比总结
sql
— 复杂的多表 JOIN 分析
SELECT
d.name as department,
COUNT(e.id) as employee_count,
AVG(e.salary) as avg_salary,
ARRAY_AGG(DISTINCT e.skills) as skills_list
FROM employees e
JOIN departments d ON e.department = d.name
JOIN projects p ON e.id = p.employee_id
WHERE e.status = ‘active’
GROUP BY d.name
HAVING COUNT(e.id) > 5
ORDER BY avg_salary DESC
LIMIT 10;
javascript
// MongoDB 聚合等效实现
db.employees.aggregate([
{ $match: { status: ‘active’ } },
{
$lookup: {
from: ‘departments’,
localField: ‘department’,
foreignField: ‘name’,
as: ‘deptInfo’
}
},
{
$group: {
_id: ‘$deptInfo.name’,
count: { $sum: 1 },
avgSalary: { $avg: ‘$salary’ },
skillsSet: { $addToSet: ‘$skills’ }
}
},
{ $match: { count: { $gt: 5 } } },
{ $sort: { avgSalary: -1 } },
{ $limit: 10 }
]);
“`
总结: MongoDB 聚合框架是处理文档数据的利器。通过熟练掌握这些操作和最佳实践,你将能够高效地完成复杂的数据分析任务,减少应用层代码的复杂性。
—
*本文档最后更新时间:2026 年 04 月 27 日*
*作者:creator | 适用 MongoDB 5.0+*



发表评论