Skip to content

聚合操作

  • 聚合操作就是通过一个方法完成一系列操作
  • 在聚合操作中,每一个操作我们称之为一个阶段,聚合操作会将上一个阶段结果传给下一个阶段,继续处理, 所有阶段处理完毕会返回一个新的结果集给我们

聚合操作格式:

db.集合名称.aggregate(<pipeline>,<options>)

  • pipeline:定义每个阶段操作
  • options:聚合操作额外配置

$project

$project: 对输入文档进行再次投影

作用:按照我们需要的格式生成结果集

格式:{$project:{<field>:<value>}}

{
    name:{firstName:'name',lastName:'Zhu'},
    age:18,
    book:{name:'html',price:12}
}

db.person.aggregate([
    {
        $project:{_id:0,myName:'$name.firstName'} #不显示_id,第二个参数相当于mysql中更改修改字段的名称
    }
])

注意点:

聚合操作不会修改原有的文档,而是返回一个新的文档给我们

如果在$project聚合操作中,是用来原有文档中不存在的字段,那么自动使用null填充

db.person.aggregate([
    {
        $project:{fullName:['$name.firstName','$name.middleName','$name.lastName']}# 中间的值并不存在,所以会直接给一个null值
    }
])

$match

$match: 和find方法中的第一个参数一样,用于筛选符合条件的文档

格式:{$match:{<query>}}

{
    name:{firstName:'name',lastName:'Zhu'},
    age:18,
    book:{name:'html',price:12}
}

db.person.aggregate([
    {
        $match:{
            'name.firstName':'name'
        }
    },
    {
        $project:{
            _id:0,
            fullName:['$name.firstName','$name.lastName'],
            myAge:'$age'
        }
    }
])

  • 以上语句的执行顺序(按管道的顺序由上至下执行)
  1. 先查询符合条件的文档

  2. 再对符合条件的文档进行处理,修改符合文档的名称

$limit和$skip

$limit : 和游标的limit方法一样,用于指定获取几个文档

格式:{$limit:<number>}

$skip : 和游标的skip一样,用于指定跳过几个文档

格式:{$skip:<number>}

db.person.aggregate([
    {$skip:1},
    {$limit:1},
    {$project:{
        _id:0,
        fullName:['$name.firstName','$name.lastName'],
        myAge:'$age'
    }}
])

注意点

在聚合操作中$limit和$skip是不同的阶段,所以要写到不同的对象中

$unwind

$unwind: 展开数组字段

{
    name:{firstName:'name',lastName:'Zhu'},
    age:18,
    book:{name:'html',price:12},
    tags:['html','js']
}

db.person.aggregate([
    {
        $unwind:{
            path:'$tags', #需要展开的字段
            includeArrayIndex:'index' #展开字段的第几个值
        }
    }
])
结果集为两个:

{
    tags:'html',
    index:NumberLong(0)
}

{
    tags:'js',
    index:NumberLong(1)
}

注意点

如果原有文档中没有需要操作的数组字段/数组字段没有元素/null,那么在$unwind阶段,会自动将这些数据过滤掉

如果不需要过滤掉需要添加一个参数:preserveNullAndEmptyArrays:true

$sort

$sort:和文档游标sort方法一致,对文档进行排序

格式:{$sort:{<field>:1|-1}}

db.集合文档.aggregate([
    {
        $sort:{
            age:1
        }
    }
])

$lookup

$lookup : 用来做关联查询的

第一种格式

格式:


{
    $lookup:{
        from:关联集合名称,
        localField:当亲集合中的字段名称
        foreignField:关联集合中的字段名称,
        as:输出字段名的名称
    }
}

db.person.insert(
    [
        {
            name:{firstName:'name',lastName:'Zhu'},
            age:18,
            books:['html','js']
        },
        {
            name:{firstName:'name1',lastName:'Zhu1'},
            age:19,
            books:['vue']
        },
        {
            name:{firstName:'name2',lastName:'Zhu2'},
            age:20,
            books:[]
        }
    ]
)

db.books.insert([
    {name:'html',price:88},
    {name:'js',price:66},
    {name:'vue',price:99}
])

# 取出每个拥有的书对应的书的详情

db.person.aggregate([
    {
        $unwind:{path:'$books'}
    },
    {$lookup:
        {
            from:'books',# 集合名称
            localField:'books',#person字段名称
            foreifnField:'name',
            as:'data'
        }
    }
])

第二种格式

格式:

{
    $lookup:{
        from:关联集合名称,
        let:{<var_1>:<expression>,...},# 定义给关联集合的聚合操作使用的当前集合的常量
        pipeline:[<pipline to execute on the collection to join>],#关联集合的聚合操作
        as :输出字段的名称
    }
}

db.person.aggregate([
    {$lookup:{
        from:'books',
        let:{bks:'$books'},
        pipeline:[
            {$match:{
                $expr:{
                    $and:[
                        {$gte:['$price',99]},
                        {$in:['$name','$$bks']} #两个$$代表定义的常量
                    ]
                }
            }}
        ],
        as:'data'
    }}
])

注意点:

在第二种$lookup阶段的第二种格式中,默认情况下是无相关查询

$group

$group:对文档进行分组

格式:


{
$group:{
    _id:<expression>,
    <field1>:{<accumulator1>:<expression>},
    .......
}
}

_id : 定义分组的规则

<field> : 定义新字段

db.person.insert([
{name:'zs', age:10, city:'北京'},
{name:'ls', age:20, city:'上海'},
{name:'ww', age:30, city:'北京'},
{name:'zl', age:40, city:'上海'},
{name:'lnj', age:50, city:'北京'},
{name:'jjj', age:60, city:'广州'},
])

db.person.aggregate([
    {$group:{
        _id:'$city',
        totalAge:{$sum:'$age'},
        avgAge:{$avg:'$age'},
        minAge:{$min:'$age'},
        maxAge:{$max:'$age'},
        totalName:{$push:'$name'}
    }}
])

$out

$out: 前面阶段处理完的文档写入一个新的集合中

{$out:'新的集合名称'}

db.person.aggregate([
    {$group:{
        _id:'$city',
        totalAge:{$sum:'$age'},
        avgAge:{$avg:'$age'},
        minAge:{$min:'$age'},
        maxAge:{$max:'$age'},
        totalName:{$push:'$name'}
    }},
    {
        $out:'newPerson'
    }
])

注意点:

如果通过$out将结果集写入到一个新的集合中,如果这个集合不存在,那么就创建一个新的集合。 如果存在集合,会覆盖掉原来的集合的文档

聚合操作的额外配置——allowDiskUse

<options>:

{allowDiskUse:<boolean>}

allowDiskUse默认取值是false,默认情况下管道阶段占用的内存不能超过100M,如果超过100M就会报错, 如果需要处理的数据比较多,聚合操作使用的内存可能超过100M,那么我们可以将 allowDiskUse设置为true,如果allowDiskUse设置为true,那么一旦超过100M 就会将操作的数据写入到临时的文件中,然后再继续操作

表达式

字段路径表达式

$<field>:使用$来指示字段路径

$<field>.<sub-field>:使用$和.来指定内嵌文档字段路径

例如:

$name

$stu.name

系统变量表达式

$$CURRENT:表示当前操作的文档

$$CURRENT.name 等价于 $name

常量表达式

将字段表达式变成一个常量字符串

使'$age'不输出值直接输出$age字符串

$literal:<value>:表示常量<value>

$literal:'$name' :表示常量字符串$name

数据类型转换操作符

MongoDB对于文档的格式并没有强制性的要求,同一个集合中存储的文档,字段的个数和数据类型都可以不同 对于文档的格式没有强制性的要求,这是MongoDB的一大优势,但是同时也增加了数据消费端的使用难度 因为我们在使用数据的时候,有可能同一个字段取出来的数据类型是不同的,这样非常不利于我们后续操作,所以 也正是因为如此,MongoDB在4.0中推出了$convert数据类型转换操作符

  • 通过$convert数据类型转换操作符,我们可以将不同的数据类型转换成相同的数据类型 以便于以后在使用数据的过程中能够统一对数据进行处理

格式:

{$convert:{
    input:'需要转换的字段',
    to:'转换之后的数据类型',
    onError:'不支持的转换类型',
    onNull:'没有需要转换的数据'
}}
db.person.insert([
{name:'zs', timestamp:ISODate('2020-08-09T11:23:34.733Z')},
{name:'ls', timestamp:'2021-02-14 12:00:06 +0800  '},
{name:'ww', timestamp:'  2023-04-01T12:00:00Z'},
{name:'zl', timestamp:'1587009270000'},
{name:'it666', timestamp:'Sunday'},
{name:'itzb'},
])

db.person.aggregate(
    [
        {$project:{
            _id:0,
            name:'$name',
            time:{
                $convert:{
                    input:'$timestamp',
                    to:'date',
                    onError:'不能转换',
                    onNull:'没有时间字段'
                }
            }    
        }
        }
    ]
)

Released under the MIT License.