$unwind和$group可实现宽表转长表:先用$map构造键值对数组,再$unwind展开,最后$project提取字段;行转列则用$group+$push聚合,配合$arrayElemAt可提取固定位置值。

用 $unwind 和 $group 实现列转行(宽表 → 长表)
MongoDB 本身没有“列转行”这种 SQL 式语法,但实际需求往往是把嵌套字段或数组展开成多条文档。典型场景是:一个文档里有多个指标字段(如 sales_q1, sales_q2, sales_q3),想转成每季度一行的结构。
关键不是硬转字段名,而是先统一结构再展开:
- 把固定字段手动映射成键值对数组,例如用
$map+$literal构造[{quarter: "Q1", value: "$sales_q1"}, ...] - 再用
$unwind展开数组 - 最后用
$project提取字段
[
{
$project: {
quarterlyData: {
$map: {
input: ["Q1", "Q2", "Q3", "Q4"],
as: "q",
in: {
quarter: "$$q",
value: {
$switch: {
branches: [
{ case: { $eq: ["$$q", "Q1"] }, then: "$sales_q1" },
{ case: { $eq: ["$$q", "Q2"] }, then: "$sales_q2" }
],
default: null
}
}
}
}
}
}
},
{ $unwind: "$quarterlyData" },
{
$project: {
_id: 0,
product: 1,
quarter: "$quarterlyData.quarter",
sales: "$quarterlyData.value"
}
}
]注意:$switch 在旧版本(< 3.4)不支持,得改用嵌套 $cond;另外 $unwind 遇到空数组或 null 会丢弃整条文档,加 preserveNullAndEmptyArrays: true 更安全。
用 $group + $push / $addToSet 实现行转列(长表 → 宽表)
这是更常见的需求:比如原始数据是每行一个用户、一个标签(user_id, tag),想聚合成每个用户一个文档、所有标签存进数组字段 tags: ["a", "b", "c"]。
核心就两步:
-
$group按目标维度(如user_id)分组 - 用
$push收集值(保留重复)、$addToSet去重
[
{
$group: {
_id: "$user_id",
tags: { $push: "$tag" }
}
},
{
$project: {
_id: 0,
user_id: "$_id",
tags: 1
}
}
]如果还要进一步把数组“摊平”成多个独立字段(比如最多 3 个标签,分别叫 tag1, tag2, tag3),就得用 $arrayElemAt 提取索引位,并配合 $ifNull 处理越界:
{
tag1: { $ifNull: [{ $arrayElemAt: ["$tags", 0] }, null] },
tag2: { $ifNull: [{ $arrayElemAt: ["$tags", 1] }, null] },
tag3: { $ifNull: [{ $arrayElemAt: ["$tags", 2] }, null] }
}处理嵌套数组时 $unwind 的顺序和性能陷阱
当文档含多层嵌套(比如 orders 数组,每个 order 又有 items 数组),$unwind 的顺序直接影响结果数量和内存消耗。
错误做法:连续两次 $unwind 同一文档,会导致笛卡尔爆炸。例如 1 个订单含 5 项商品,10 个订单就生成 50 行;若再按用户 unwind,可能瞬间放大百倍。
正确策略:
- 先在最内层聚合(如用
$reduce或$sum把 items 合并为总价),减少中间文档数 - 必须展开时,用
$unwind后立刻$group回收,别让膨胀数据流到后续阶段 - 对大数据集,考虑在应用层分页处理,而不是靠管道硬扛
另外,$unwind 不支持直接展开根级字段(如 $unwind: "$tags" 可以,但 $unwind: "tags" 会报错:必须带 $ 前缀)。
用 $pivot?别找——MongoDB 没有这个操作符
很多人搜 “MongoDB pivot”,期待类似 SQL 的 PIVOT 语法,但 MongoDB 聚合框架至今(7.0)没有内置 $pivot。所有“行列互转”本质都是手工构造键值对 + 分组/展开。
常见误区:
- 试图用
$cond+ 多个$sum模拟列转行:可行但字段必须提前枚举,无法动态适应新列名 - 依赖驱动层(如 Node.js)做后处理:容易内存溢出,且丢失管道优化能力(如索引下推、早期过滤)
真正灵活的做法是:把“列名”也作为数据字段存入(比如改成 {metric: "sales_q1", value: 100}),这样聚合时就能用 $group + $arrayToObject 动态组装宽表:
{
$group: {
_id: "$product",
metrics: { $push: { k: "$metric", v: "$value" } }
}
},
{
$project: {
_id: 0,
product: "$_id",
data: { $arrayToObject: "$metrics" }
}
}这要求数据建模阶段就接受“属性动态化”,而不是强求 schema 固定字段。
字段是否该扁平化、是否该用嵌套数组,往往比怎么转更重要——重构聚合逻辑前,先看一眼文档结构是否适配查询模式。

















