MongoDB聚合管道提供了丰富的数组操作符,比如常见的$map、$filter、$slice等,但有一个操作符经常被开发者忽略,它就是$zip。这个操作符的功能类似于Python内置的zip函数:把多个数组按索引位置配对,最终生成一个由子数组组成的二维数组。当你需要在文档中同时遍历两个或多个相互对应的数组时,$zip往往是比$unwind加$group更优雅的解决方案。

$zip的基本语法与工作原理
$zip的语法结构并不复杂,它接收一个对象作为参数,该对象包含三个字段:inputs是必填项,类型为数组,数组中的每个元素都是一个表达式,求值结果必须是可以被$ifNull处理的值(通常是数组);useLongestLength是可选的布尔值,默认为false;defaults同样是可选项,用于补齐较短数组的默认值。
在没有设置任何可选参数的情况下,$zip的行为是以最短的输入数组为基准进行截断。也就是说,如果输入了两个数组,一个长度为5,另一个长度为3,那么输出结果的长度就是3,较长数组中多出来的元素会被直接丢弃。这个特性和Python的zip函数完全一致。
看一个最基础的例子,假设文档中有months和sales两个数组,分别存储月份和对应的销售额:
db.orders.aggregate([
{
$project: {
paired: {
$zip: {
inputs: ["$months", "$sales"]
}
}
}
}
])
如果months是["1月", "2月", "3月"],sales是[120, 85, 200],那么输出结果就是:[["1月", 120], ["2月", 85], ["3月", 200]]。可以看到,两个数组按照索引位置配对成了一个个的二元子数组,这就是$zip最核心的能力。
需要注意的一点是,inputs中的每个表达式在求值后必须是数组,如果某个输入解析出来是null或者不是数组的值,$zip会直接报错。因此在处理可能缺失的字段时,建议先用$ifNull给字段兜底,例如写成{$ifNull: ["$sales", []]},这样即使字段不存在也不会导致整个管道崩溃。
处理长度不一致的数组:useLongestLength与defaults
实际业务中,数组长度不一致的情况非常常见。比如一份成绩单文档,subjects记录了所有科目,而部分学生的scores只填了一部分。这时如果直接使用默认的$zip,超出部分的数据会丢失,显然不是我们想要的结果。
这时就需要用到useLongestLength参数。把它设置为true之后,$zip会以最长的输入数组为基准来生成结果,长度不足的数组对应位置会用defaults中指定的默认值填充。来看一个完整的示例:
db.students.aggregate([
{
$project: {
gradeSheet: {
$zip: {
inputs: ["$subjects", "$scores"],
useLongestLength: true,
defaults: ["缺考"]
}
}
}
}
])
假设subjects为["语文", "数学", "英语"],scores为[90],输出结果为[["语文", 90], ["数学", "缺考"], ["英语", "缺考"]]。可以看到defaults数组按照顺序逐个填充了空缺的位置。
使用defaults时有几个细节必须留意。首先,如果设置了defaults,那么useLongestLength必须同时为true,否则MongoDB会抛出错误;其次,defaults数组的长度必须等于或大于“最长数组长度减去该数组长度”的差值,换句话说要有足够的默认值可用;最后,defaults中的默认值类型不必与原数组元素一致,可以是任意合法的BSON值,包括null、字符串甚至嵌套文档,这给了业务处理很大的灵活性,比如可以用一个特殊标记值方便后续统计。
$zip与其他操作符的组合实战
$zip单独使用的输出是二维数组,很多时候我们还需要进一步加工。最经典的组合是$zip加$arrayToObject,可以直接把两个数组压缩成键值对形式的文档。
比如有一个配置文档,keys数组存储配置项名称,values数组存储对应的值,希望把它们合并成一个对象方便查询:
db.configs.aggregate([
{
$project: {
configMap: {
$arrayToObject: {
$zip: {
inputs: ["$keys", "$values"]
}
}
}
}
}
])
如果keys为["host", "port"],values为["127.0.0.1", 27017],那么最终结果就是{host: "127.0.0.1", port: 27017}。注意$zip生成的子数组顺序正好满足$arrayToObject对[键, 值]对的要求,这个组合在处理动态字段名、宽表转窄表等场景中非常好用。
另一个常见组合是$zip加$map。$zip负责把多个数组按位置绑定,$map负责对每个绑定的结果做进一步计算。比如要计算每个月销售额的环比增长率:
db.sales.aggregate([
{
$project: {
growthRate: {
$map: {
input: {
$zip: {
inputs: [
{ $slice: ["$months", 1] },
{ $slice: ["$sales", 1] },
"$sales"
]
}
},
as: "item",
in: {
month: { $arrayElemAt: ["$$item", 0] },
rate: {
$let: {
vars: {
prev: { $arrayElemAt: ["$$item", 1] },
curr: { $arrayElemAt: ["$$item", 2] }
},
in: {
$cond: [
{ $eq: ["$$prev", 0] },
null,
{ $divide: [{ $subtract: ["$$curr", "$$prev"] }, "$$prev"] }
]
}
}
}
}
}
}
}
}
])
这个例子中,$zip同时接收三个输入数组,其中前两个通过$slice从下标1开始截取,第三个是完整的销售额数组。这样压缩之后,每个子数组的第一个元素是当前月份,第二个是上月销售额,第三个是本月销售额,$map再逐一计算增长率。当分母为零时用$cond返回null,避免除零错误。这个例子充分展示了$zip支持多数组输入的特性,它并不局限于两个数组,理论上可以同时压缩任意数量的数组。
此外,$zip还可以配合$reduce对压缩后的结果做累计运算,或者配合$filter过滤掉不符合条件的配对项。在监控数据处理、时序指标对齐、动态表单解析等场景中,这种“先压缩再加工”的思路都能显著简化聚合逻辑,避免了$unwind展开后再$group还原这种绕圈子的写法,既提升了管道可读性,也减少了中间数据量,性能上往往更优。
总结一下,$zip适合的场景可以归纳为:多个数组元素按位置一一对应、需要在管道内原位完成配对处理、不希望改变文档结构。掌握它之后,很多原本要拆到应用层处理的数组逻辑都可以下沉到数据库端完成,值得在日常开发中优先考虑。
MongoDB聚合管道$zip数组操作修改时间:2026-09-11 12:02:41