MongoDB聚合:$unwind

使用$unwind可以对输入文档数组字段进行结构,为每个数组元素输出一个文档,并且每个输出文档都是输入文档数组字段的值。相当于将内嵌的数组文档提升到了顶层。

语法

可以通过字段路径或文档操作符来展开数组字段。

通过字段操作符展开

可以将数组字段路径传递给$unwind,如果字段值为null、不存在或者是个空数组,则$unwind不会输出文档。

指定的字段路径时,字段名前面必须使用$前缀,并用引号引起来。

{ $unwind:  }

使用文档操作符选项

可以通过给$unwind传递一个文档来指定展开时的行为:

{ $unwind:
    { path: ,
      includeArrayIndex: ,
      preserveNullAndEmptyArrays:  }
}

|字段|类型|说明|

|path|string|数组字段的路径,字段名前面必须使用$前缀,并用引号引起来|

|includeArrayIndex|string|可选,给数组元素位置字段指定字段名,不能以$开始|

|preserveNullAndEmptyArrays|boolean|可选,缺省值为false。如果为true,当path为空、不存在或数组元素为空,$unwind输出文档,否则如果为false则不输出文档|

使用

非数组字段路径

  • 当操作符没有解析为数组,但不是缺失、空或空数组时,$unwind会将操作符视为单元素数组。

  • 当操作符为空、缺失或空数组时,$unwind会按照preserveNullAndEmptyArrays选项执行。

    字段缺失

    如果输入文档中不存在指定字段的路径或字段是个空数组,默认情况下$unwind会忽略输入文档且不输出文档。可以使用preserveNullAndEmptyArrays选项来强制输出文档。

    举例

    展开数组

    为inventory文档添加一个文档:

    db.inventory.insertOne({ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] })
    

    下面的聚合使用$unwind阶段为sizes数组的每个元素输出一个文档:

    db.inventory.aggregate( [ { $unwind : "$sizes" } ] )
    

    操作返回:

    { "_id" : 1, "item" : "ABC1", "sizes" : "S" }
    { "_id" : 1, "item" : "ABC1", "sizes" : "M" }
    { "_id" : 1, "item" : "ABC1", "sizes" : "L" }
    

    每个文档都与输入文档完全相同,只是sizes字段的值不同,该字段的值来自原始sizes数组。

    缺失或数组为空

    准备clothing集合数据:

    db.clothing.insertMany([
      { "_id" : 1, "item" : "Shirt", "sizes": [ "S", "M", "L"] },
      { "_id" : 2, "item" : "Shorts", "sizes" : [ ] },
      { "_id" : 3, "item" : "Hat", "sizes": "M" },
      { "_id" : 4, "item" : "Gloves" },
      { "_id" : 5, "item" : "Scarf", "sizes" : null }
    ])
    

    下列情况下,$unwind把sizes字段看做一个文档:

    • 字段存在
    • 字段的值不为空
    • 字段的值不是空数组

      使用$unwind展开sizes数组:

      db.clothing.aggregate( [ { $unwind: { path: "$sizes" } } ] )
      

      操作返回:

      { _id: 1, item: 'Shirt', sizes: 'S' },
      { _id: 1, item: 'Shirt', sizes: 'M' },
      { _id: 1, item: 'Shirt', sizes: 'L' },
      { _id: 3, item: 'Hat', sizes: 'M' }
      
      • 在_id: 1的文档中,sizes是一个有元素的数组,$unwind为sizes字段的每个元素生成了一个文档
      • 在_id: 3的文档中,sizes被解析为只有一个元素的数组
      • _id: 2、_id: 4和_id: 5文档则没有返回,因为sizes字段不能被解析为单元素数组。

        注意:{path: }是可选的,下面的两个$unwind操作是等价的:

        db.clothing.aggregate( [ { $unwind: "$sizes" } ] )
        db.clothing.aggregate( [ { $unwind: { path: "$sizes" } } ] )
        

        preserveNullAndEmptyArrays 和 includeArrayIndex

        创建inventory2并插入下面数据:

        db.inventory2.insertMany([
           { "_id" : 1, "item" : "ABC", price: NumberDecimal("80"), "sizes": [ "S", "M", "L"] },
           { "_id" : 2, "item" : "EFG", price: NumberDecimal("120"), "sizes" : [ ] },
           { "_id" : 3, "item" : "IJK", price: NumberDecimal("160"), "sizes": "M" },
           { "_id" : 4, "item" : "LMN" , price: NumberDecimal("10") },
           { "_id" : 5, "item" : "XYZ", price: NumberDecimal("5.75"), "sizes" : null }
        ])
        
        preserveNullAndEmptyArrays

        下面的$unwind操作使用preserveNullAndEmptyArrays选项,当sizes字段为null、缺失或空数组时,仍然输出文档。

        db.inventory2.aggregate( [
           { $unwind: { path: "$sizes", preserveNullAndEmptyArrays: true } }
        ] )
        

        输出包含了sizes为null、缺失或为空数组时的文档:

        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "S" }
        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "M" }
        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "L" }
        { "_id" : 2, "item" : "EFG", "price" : NumberDecimal("120") }
        { "_id" : 3, "item" : "IJK", "price" : NumberDecimal("160"), "sizes" : "M" }
        { "_id" : 4, "item" : "LMN", "price" : NumberDecimal("10") }
        { "_id" : 5, "item" : "XYZ", "price" : NumberDecimal("5.75"), "sizes" : null }
        
        includeArrayIndex

        下面的$unwind操作使用includeArrayIndex选项,在输出文档中添加了数组元素索引字段。

        db.inventory2.aggregate( [
          { $unwind:
              { path: "$sizes",
                includeArrayIndex: "arrayIndex"
              }
           }])
        

        操作展开了sizes数组,并为输出文档增加了一个数组元素索引字段arrayIndex,如果sizes字段不能被解析为一个有元素的数组,但又不是缺失、为空或空数组,则arrayIndex字段为空。

        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "S", "arrayIndex" : NumberLong(0) }
        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "M", "arrayIndex" : NumberLong(1) }
        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "L", "arrayIndex" : NumberLong(2) }
        { "_id" : 3, "item" : "IJK", "price" : NumberDecimal("160"), "sizes" : "M", "arrayIndex" : null }
        

        对展开的数组做分组汇总

        创建并填充inventory2集合:

        db.inventory2.insertMany([
          { "_id" : 1, "item" : "ABC", price: NumberDecimal("80"), "sizes": [ "S", "M", "L"] },
          { "_id" : 2, "item" : "EFG", price: NumberDecimal("120"), "sizes" : [ ] },
          { "_id" : 3, "item" : "IJK", price: NumberDecimal("160"), "sizes": "M" },
          { "_id" : 4, "item" : "LMN" , price: NumberDecimal("10") },
          { "_id" : 5, "item" : "XYZ", price: NumberDecimal("5.75"), "sizes" : null }
        ])
        

        下面的操作展开sizes数组,并对展开文档的size字段进行分组:

        db.inventory2.aggregate( [
           //阶段1
           { $unwind: { path: "$sizes", preserveNullAndEmptyArrays: true }
           },
           //阶段2
           { $group:
               { _id: "$sizes",
                 averagePrice: { $avg: "$price" }
               }
           },
           //阶段3
           { $sort: { "averagePrice": -1 }
           }
        ] )
        
        阶段1

        $unwind阶段为sizes数组的每个元素输出一个文档,该阶段使用preserveNullAndEmptyArrays选项为sizes字段为空、缺失或空数组的情况都输出了文档,该阶段完成后输出下面的文档进入下一阶段:

        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "S" }
        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "M" }
        { "_id" : 1, "item" : "ABC", "price" : NumberDecimal("80"), "sizes" : "L" }
        { "_id" : 2, "item" : "EFG", "price" : NumberDecimal("120") }
        { "_id" : 3, "item" : "IJK", "price" : NumberDecimal("160"), "sizes" : "M" }
        { "_id" : 4, "item" : "LMN", "price" : NumberDecimal("10") }
        { "_id" : 5, "item" : "XYZ", "price" : NumberDecimal("5.75"), "sizes" : null }
        
        阶段2

        $group阶段根据sizes进行分组,并计算所有尺寸价格的平均值。该阶段完成后,生成下面的文档进入下一阶段:

        { "_id" : "S", "averagePrice" : NumberDecimal("80") }
        { "_id" : "L", "averagePrice" : NumberDecimal("80") }
        { "_id" : "M", "averagePrice" : NumberDecimal("120") }
        { "_id" : null, "averagePrice" : NumberDecimal("45.25") }
        
        阶段3

        $sort阶段根据averagePrice字段值从大到小对文档进行排序,该阶段完成后返回下面的文档:

        { "_id" : "M", "averagePrice" : NumberDecimal("120") }
        { "_id" : "L", "averagePrice" : NumberDecimal("80") }
        { "_id" : "S", "averagePrice" : NumberDecimal("80") }
        { "_id" : null, "averagePrice" : NumberDecimal("45.25") }