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

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
});


---

总结与最佳实践

核心要点回顾

  1. 聚合框架优势:强大的数据转换能力,无需应用层处理
  2. $match 优化:尽早使用,充分利用索引
  3. $group 灵活:支持多维度分组和聚合计算
  4. $lookup 强大:实现文档关联,类似 SQL JOIN
  5. 数组操作:$unwind、$push、$addToSet 处理嵌套数据
  6. 性能优先:合理的管道顺序至关重要
  7. 最佳实践清单

    设计原则
    • 将 `$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+*

标签

发表评论