Descobrindo a quantidade de elementos, limitando, ordenando e distinguindo valores de uma collection, agrupando.

MongoDB - Aggregate e Groups

LOL Vamos continuar essa série pra ficar mais hard core agora legal com esses operadores.

Antes de fatos aprendermos sobre Agreggate e Groups, vamos ver alguns tópicos importantes. Para seguir os primeiros exemplos vamos usar a collection restaurantes use o restaurantes.json para importar.【ツ】

Como saber a quantidade de documentos que eu tenho?

Podemos usar a função length:

 > db.restaurantes.find().length()

Porém dessa forma o resultado é ‘lento’, para isso temos uma função própria que é muito mais rápida que o length( )

 > db.restaurantes.count()

Resultado:

> 25359

Com o count( ) também podemos usar querys que usamos no find( ) para saber a quantidade do que queremos.

> var query = {borough:'Bronx'}
> db.restaturantes.count(query)

Resultado:

> 2338

Distinct

Neste nosso restaurantes temos a seguinte estrutura:

  {
  "_id": ObjectId("564d6c5af9e09ea567dade51"),
  "address": {
    "building": "2780",
    "coord": [
      -73.98241999999999,
      40.579505
    ],
    "street": "Stillwell Avenue",
    "zipcode": "11224"
  },
  "borough": "Brooklyn",
  "cuisine": "American ",
  "grades": [
    {
      "date": ISODate("2014-06-10T00:00:00Z"),
      "grade": "A",
      "score": 5
    },
    {
      "date": ISODate("2013-06-05T00:00:00Z"),
      "grade": "A",
      "score": 7
    },
    {
      "date": ISODate("2012-04-13T00:00:00Z"),
      "grade": "A",
      "score": 12
    },
    {
      "date": ISODate("2011-10-12T00:00:00Z"),
      "grade": "A",
      "score": 12
    }
  ],
  "name": "Riviera Caterer",
  "restaurant_id": "40356018"
}

Repare, e se quisermos saber todas as borough nesse universo de 25359 documentos? Precisamos saber encontrar valores únicos de uma propriedade, pois ela pode se repetir em outros documentos. O distinct( ) vai nos ajudar com isso.

> db.restaurantes.distinct('borough')

Resultado:

[
  "Brooklyn",
  "Manhattan",
  "Queens",
  "Staten Island",
  "Bronx",
  "Missing"
]

Ora se ele retorna um array, e usamos JavaScript, podemos usar um length para saber a quantidade não é ?

> db.restaurantes.distinct('borough').length

Resultado:

> 6

Tem como trazer o resultado ordenado ?

Quem ae já estudou os algoritmos de ordenação, sabe que dá muita raiva sabe que existe vários algoritmos de ordenação e com vários casos cada um. Poxa e agora ? se lascou, vai ter que implementar uma função para isso HEYY!!! Lembre, se estamos usando JavaScript, podemos usar a função sort( ) para isso! e para os curiosos, ele usa o algorimo MergeSort, veja o código aqui.

> db.restaurantes.distinct('borough').sort()

Resultado:

[
  "Bronx",
  "Brooklyn",
  "Manhattan",
  "Missing",
  "Queens",
  "Staten Island"
]

Porém se quiser fazer o contrário, use a função reverse( )

> db.restaurantes.distinct('borough').sort().reverse()

Limite

Agora vamos usar uma collection pokemons nos próximos exemplos, use o pokemons.json para importar.

As vezes podemos querer colocar um limite de busca, assim limitando o número de resultados, como por exemplo:

> db.pokemons.find({}, {name:1, _id:0}).limit(3)

Resultado:

{
  "name": "Pidgeotto"
}
{
  "name": "Raticate"
}
{
  "name": "Fearow"
}

Com ele podemos também usar o skip( ), para pedir para pular um determinado valor.

> db.pokemons.find({}, {name:1, _id:0}).limit(5)
 .skip(2)

Assim ele pulou os 2 primeiros pokemons e retornou os próximos 5. Podemos fazer uma paginação com ele, assim mostrando de 5 em 5 por exemplo.

> db.pokemons.find({}, {name:1, _id:0}).limit(5).skip(5*0)]
> db.pokemons.find({}, {name:1, _id:0}).limit(5).skip(5*1)
> db.pokemons.find({}, {name:1, _id:0}).limit(5).skip(5*2)

Distinct

Geralmente em uma collection que possui muitos documentos, contém propriedades com valores iguais. Nesse exemplo de pokemons, repare que não existe um exclusivo type pra cada um, ele se repete em outros. Vamos descobrir quais e quantos types de pokemons existe na collection:

> db.pokemons.distinct('types').length
> db.pokemons.distinct('types')

Resultado:

[
  "bug",
  "poison",
  "flying",
  "normal",
  "electric",
  "water",
  "fighting",
  "psychic",
  "grass",
  "fairy",
  "fire",
  "rock",
  "ice",
  "ground",
  "steel",
  "ghost",
  "dark",
  "dragon"
]

Agrupamento

Podemos agrupar cada tipo de pokemons e poder mandar contar quantos pokemons tem aquele valor por exemplo, tudo isso usando a função group( ).

O group tem no total 6 propriedades que podemos utilizar, vamos usar algumas delas para nossos exemplos.

  1. key
  2. reduce
  3. initial
  4. keyf
  5. cond
  6. finalize

Vamos ver algumas delas! (͡๏̯͡๏)۶

//exemplo 1
db.pokemons.group({
  initial: {total : 0},
  reduce : function (curr, result){
    curr.types.forEach(function(type){
      if(result[type]){
        result[type]++;
      }else{
        result[type] = 1;
      }
      result.total++;
    });
  }
});

Resultado:

[
  {
    "total": 934,
    "normal": 79,
    "flying": 81,
    "poison": 54,
    "bug": 58,
    "electric": 47,
    "water": 101,
    "fighting": 42,
    "psychic": 61,
    "grass": 70,
    "fairy": 31,
    "fire": 53,
    "rock": 42,
    "ice": 28,
    "ground": 53,
    "steel": 35,
    "ghost": 34,
    "dark": 35,
    "dragon": 30
  }
]

Entendo o exemplo 1

Usamos algumas propriedades do group, que é o init e o reduce: No init inicializamos a variável total para contar e em seguida o reduce onde toda a mágica acontece. No reduce escrevemos uma função que recebe o curr como parâmetro e será de fato cada objeto, e o result que é responsável pelo resultado, em seguida acesso o tipo de cada pokemon e faço um forEach para descobrir quantos pokemons tem esse tipo.

Colocando condições

Vamos usar a propriedade cond para informar nossa condição.

db.pokemons.group({
  initial:{total : 0},
  cond   :{defense: {$gt:200}},
  reduce :function (curr, result){
    curr.types.forEach(function(type){
      if(result[type]){
        result[type]++;
      }else{
        result[type] = 1;
      }
      result.total++;
    });
  }
});

Resultado:

[
  {
    "total": 2,
    "rock": 1,
    "bug": 1
  }
]

Finalize

Ao contrários das outras propriedades que é sempre chamada para cada documento, o finalize é chamada apenas ao final da execução.

//exemplo 2
db.pokemons.group({
  initial: {total: 0, defense: 0, attack:0},
  reduce : function (current, result){
    result.total++;
    result.defense += current.defense;
    result.attack  += current.attack;
    },
  finalize: function(result){
     result.media_defense = result.defense/result.total;
     result.media_attack  = result.attack/result.total;
  }
})

Resultado:

[
  {
    "total": 620,
    "defense": 44363,
    "attack": 47022,
    "media_defense": 71.55322580645161,
    "media_attack": 75.84193548387097
  }
]

Exemplo 2 ? WTF Usamos o finalize para sabermos a média de attacks e defenses, pra isso ao final de tudo pegamos a soma deles e dividimos pela quantidade. Simples não é? ( ͡° ͜ʖ ͡°)

Aggregate

Temos 3 abordagens para agregações, cada uma com sua característica e propósitos para cada situação, veremos a aggregation pipeline.

Aggregation Pipeline ?

Ele é basicamente um framework para executar uma série de transformações de dados em um documento. Existe 10 tipos de transformações que podem ser utilizados.

  1. $geoNear
  2. $match
  3. $project
  4. $redact
  5. $unwind
  6. $group
  7. $limit
  8. $skip
  9. $sort
  10. $out

Vamos ver alguns deles para nossos exemplos. =)
Similar ao group, podemos fazer a mesma coisa em menor tamanho de linhas. Basicamente tudo que fazemos com o group, podemos fazer com o aggregate.

//exemplo 3
db.pokemons.aggregate({
  $group:{
      _id:{},
      media_defense:{ $avg: '$defense'},
      media_atack: {$avg: '$attack'},
      defense: {$sum: '$defense'},
      attack: {$sum: '$attack'},
      total: {$sum: 1}
  }
})

Resultado:

{
  "result": [
    {
      "_id": {

      },
      "media_defense": 71.55322580645161,
      "media_atack": 75.84193548387097,
      "defense": 44363,
      "attack": 47022,
      "total": 620
    }
  ],
  "ok": 1
}

Você vai ver que deu o mesmo valor do exemplo 1

Entendendo o exemplo 3

No aggregate tenho uma propriedade chamada $group, aliás todas as funções do aggregate começa com o $. O _id serve para definir o agrupamento, por exemplo se eu quisesse agrupar por data ou tempo, mas como nesse exemplo não vamos precisar separar nada, fica vazio. Usei a função $avg para trazer a média da coluna defense e attack, a $sum foi para trazer o somatório de cada, é importante destacar que ao definir a coluna precisamos colocar o $ no início para informar o mongo que ela é uma coluna.

Você deve ter reparado na facilidade que deu usando esses operadores $sum e $svg não é ? Pois bem, eles são operadores acumuladores, temos 10 no total que salva sua vida. ٩(-̮̮̃•̃)۶

  1. $sum
  2. $avg
  3. $first
  4. $last
  5. $max
  6. $min
  7. $push
  8. $addToSet
  9. $stdDevPop
  10. $stdDevSamp

Como no outro exemplo fiz uma condição, no aggregate também podemos.

//exemplo 4
db.pokemons.aggregate([
{$match: {types: 'fire'}},
{
  $group:{
      _id:{},
      media_defense:{ $avg: '$defense'},
      media_atack: {$avg: '$attack'},
      defense: {$sum: '$defense'},
      attack: {$sum: '$attack'},
      total: {$sum: 1}
  }
}])

Resultado:

{
  "result": [
    {
      "_id": {

      },
      "media_defense": 67.20754716981132,
      "media_atack": 78.88679245283019,
      "defense": 3562,
      "attack": 4181,
      "total": 53
    }
  ],
  "ok": 1
}

Group e Aggregate são conceitos muitos importantes e usados. Na documentação você pode encontrar mais exemplos.

Concluindo

Aproveite para ler:

MongoDB Aggregation and Data Processing

MongoDB Aggregation Framework Principles and Examples

E é isso, pratique e pratique, até a próxima.