Skip to content

Instantly share code, notes, and snippets.

@kaminski-tomasz
Last active January 17, 2024 13:22
Show Gist options
  • Save kaminski-tomasz/25ea72898921c705cd8cf8acc87933aa to your computer and use it in GitHub Desktop.
Save kaminski-tomasz/25ea72898921c705cd8cf8acc87933aa to your computer and use it in GitHub Desktop.

Rozdział 0 - Wprowadzenie

Połączenie z MongoDB (z kursu):

mongo "mongodb://cluster0-shard-00-00-jxeqq.mongodb.net:27017,cluster0-shard-00-01-jxeqq.mongodb.net:27017,cluster0-shard-00-02-jxeqq.mongodb.net:27017/aggregations?replicaSet=Cluster0-shard-0" \
    --authenticationDatabase admin \
    --ssl -u m121 -p aggregations --norc

Polecenia:

  • Wylistowanie baz danych: show dbs
  • Wylistowanie kolekcji (aktualna db):
Cluster0-shard-0:PRIMARY> show collections
air_airlines
air_alliances
air_routes
bronze_banking
customers
employees
exoplanets
gold_banking
icecream_data
movies
nycFacilities
silver_banking
solarSystem
stocks
system.views

Pipeline:

  1. to composition of stages - kompozycja etapów przetwarzania
  2. każdy stage jest konfigurowalny do transformacji danych.
  3. dokumenty przepływają przez etapy jak na linii produkcyjnej.
  4. etapy można układać na dowolne sposoby.

Składnia agregacji

db.userColl.aggregate([
    {stage1},
    {stage2},
    {...stageN}
], {options})

Przykład:

// simple first example
db.solarSystem.aggregate([{
    "$match": {
        "atmosphericComposition": {"$in": [/O2/]},
        "meanTemperature": {$gte: -40, "$lte": 40}
    }
}, {
    "$project": {
        "_id": 0,
        "name": 1,
        "hasMoons": {"$gt": ["$numberOfMoons", 0]}
    }
}], {"allowDiskUse": true});

Aggregate operators: $match, $project Query operators: $in, $gte

Operatory zawsze występują jako klucze w dokumencie zapytania. Wyrażenia zawsze występują jako wartości.

$gt - może być operatorem (np. w $match), może być funkcją/wyrażeniem (np. w $project)

  • temperature: {$gt: 10} - tutaj jako query operator
  • hasMoons: {$gt: ["$numberOfMoons", 0]} - tutaj jako wyrażenie

Dostęp do danych w wyrażeniach:

  • ścieżka do pola: "$fieldName"
  • zmienna systemowa: "$$UPPERCASE" ("$$CURRENT" - bieżący dokument)
  • zmienna użytkownika: "$foo"

Struktura & reguły składniowe

  • Pipeline - to zawsze tablica 1 lub więcej stage'ów
  • Etapy są skomponowane z jednego lub więcej operatorów agregacyjnych albo wyrażeń
    • wyrażenie może przyjmować jeden argument lub tablicę - to zależy od typu wyrażenia

Rozdział 1 - Podstawowe agregacje: $match i $project

$match

Lepiej jest o nim myśleć jako o filter niż jako o find, chociaż używa tej samej składni. Czyli: konfigurujemy filtry na etapie $match i dokumenty, które spełniają kryteria filtrów, przechodzą dalej w przetwarzaniu pipeline'a.

  • $match używa standardowych operatorów zapytań w MongoDB. Możemy używać dowolnych; możemy używać dopasowań bazujących na porównaniach, logice i tablicach; nie możemy używać operatora $where.

  • jeśli chcemy użyć operatora $test, $match musi być pierwszym etapem w pipeline.

  • jeśli $match jest pierwszym etapem, może wykorzystać indeksy, co przyspieszy przetwarzanie całej operacji.

  • $match nie pozwala na projekcję danych; jest ona możliwa na innym etapie.

Przykładowe zapytania:

// $match all celestial bodies, not equal to Star
db.solarSystem.aggregate([{
    "$match": {"type": {"$ne": "Star"}}
}]).pretty()

// same query using find command
db.solarSystem.find({"type": {"$ne": "Star"}}).pretty();

// count the number of matching documents
db.solarSystem.count();

// using $count
db.solarSystem.aggregate([{
    "$match": {"type": {"$ne": "Star"}}
}, {
    "$count": "planets"
}]);

// matching on value, and removing ``_id`` from projected document
db.solarSystem.find({"name": "Earth"}, {"_id": 0});

Lab: $match

My answer:

var pipeline = [
    {
        $match: {
            "imdb.rating": {$gte: 7},
            $and: [
                {genres: {$ne: "Crime"}},
                {genres: {$ne: "Horror"}}
            ],
            rated: {$in: ["PG", "G"]},
            languages: {$all: ["English", "Japanese"]},
        }
    }
];

Correct answer:

var pipeline = [
    {
        $match: {
            "imdb.rating": {$gte: 7},
            genres: {$nin: ["Crime", "Horror"]},
            rated: {$in: ["PG", "G"]},
            languages: {$all: ["English", "Japanese"]}
        }
    }
]

Z dokumentacji $nin:

Syntax: { field: { $nin: [ , ... ]} }

If the field holds an array, then the $nin operator selects the documents whose field holds an array with no element equal to a value in the specified array (e.g. , , etc.).

$project

Składnia:

{
  $project: {
    <specification(s)>
  }
}
  • Jeżeli określimy pole, które chcemy zachować, musimy też podać wszystkie inne pola, które też mają być zachowane. Jedynym wyjątkiem jest _id.
    • np. {$project: {fieldA: 1, fieldB: 1}}
  • _id trzeba też jawnie usunąć z wyniku, jeśli jest to pożądane: {_id: 0}
  • Poza prostym usuwaniem/pozostawieniem pól, $project pozwala definiować nowe pola.
  • $project może być użyty wielokrotnie w całym aggregation pipeline.
  • $project może być użyty do przepisywania wartości pól do nowych, lub do tworzenia nowych na podstawie istniejących wartości.

Przykłady:

// project ``name`` and remove ``_id``
db.solarSystem.aggregate([{"$project": {"_id": 0, "name": 1}}]);

// project ``name`` and ``gravity`` fields, including default ``_id``
db.solarSystem.aggregate([{"$project": {"name": 1, "gravity": 1}}]);

// using dot-notation to express the projection fields
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "gravity.value": 1
    }
}]);

// reassing ``gravity`` field with value from ``gravity.value`` embeded field
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "gravity": "$gravity.value"
    }
}]);

// creating a document new field ``surfaceGravity``
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "surfaceGravity": "$gravity.value"
    }
}]);

// creating a new field ``myWeight`` using expressions
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "myWeight": {"$multiply": [{"$divide": ["$gravity.value", 9.8]}, 86]}
    }
}]);

Lab: $project

My answer:

var aggregation = [
    {
        $match: {
            "imdb.rating": {$gte: 7},
            $and: [
                {genres: {$ne: "Crime"}},
                {genres: {$ne: "Horror"}}
            ],
            rated: {$in: ["PG", "G"]},
            languages: {$all: ["English", "Japanese"]},
        }
    },
    {
        $project: {
            _id: 0,
            title: 1,
            rated: 1
        }
    }
]

Correct answer:

var pipeline = [
    {
        $match: {
            "imdb.rating": {$gte: 7},
            genres: {$nin: ["Crime", "Horror"]},
            rated: {$in: ["PG", "G"]},
            languages: {$all: ["English", "Japanese"]}
        }
    },
    {
        $project: {_id: 0, title: 1, "rated": 1}
    }
]

Lab: Computing fields

My answer:

db.movies.aggregate([
    {
        $project: {
            titleWordsCount: {$size: {$split: ["$title", ' ']}}
        }
    },
    {
        $match: {
            titleWordsCount: {$eq: 1}
        }
    }
]).itcount()

Correct answer:

db.movies.aggregate([
    {
        $match: {
            title: {
                $type: "string"
            }
        }
    },
    {
        $project: {
            title: {$split: ["$title", " "]},
            _id: 0
        }
    },
    {
        $match: {
            title: {$size: 1}
        }
    }
]).itcount()

$map

$map to operator, który potrafi zmapować tablicę. Składnia:

 $map: {
    input: "$writers",  // wyrażenie dające w wyniku tablicę   
    as: "writer",       // opcjonalna nazwa zmiennej - iteratora (domyślnie odwołujemy się przez $$this)
    in: "$$writer"      // wyrażenie mapujące element w tablicy
  }

Przykład:

db.movies.aggregate([
    {
        $project: {
            cast: 1,
            writers: {
                $map: {
                    input: "$writers",
                    as: "writer",
                    in: {
                        $arrayElemAt: [
                            {
                                $split: ["$$writer", " ("]
                            },
                            0
                        ]
                    }
                }
            },
            _id: 0
        }
    },
])

$setIntersection

$setIntersection - przecięcie zbiorów; parametrem są wyrażenia ewaluujące się do tablic elementów, z których obliczana jest część wspólna. Jeżeli nie znaleziono części wspólnej, zwracana jest pusta tablica.

Zadanie:

Let's find how many movies in our movies collection are a "labor of love", 
where the same person appears in cast, directors, and writers.

My answer:

db.movies.aggregate([
    {
        $project: {
            _id: 0,
            laborsOfLove: {
                $setIntersection: [
                    "$directors", "$cast", {
                        $map: {
                            input: "$writers",
                            as: "writer",
                            in: {
                                $arrayElemAt: [
                                    {
                                        $split: ["$$writer", " ("]
                                    },
                                    0
                                ]
                            }
                        }
                    }
                ]
            }
        }
    },
    {
        $match: {
            laborsOfLove: {$elemMatch: {$exists: true}}
        }
    },
    {
        $count: "labors of love"
    }
])

Correct answer:

db.movies.aggregate([
    {
        $match: {
            cast: {$elemMatch: {$exists: true}},
            directors: {$elemMatch: {$exists: true}},
            writers: {$elemMatch: {$exists: true}}
        }
    },
    {
        $project: {
            _id: 0,
            cast: 1,
            directors: 1,
            writers: {
                $map: {
                    input: "$writers",
                    as: "writer",
                    in: {
                        $arrayElemAt: [
                            {
                                $split: ["$$writer", " ("]
                            },
                            0
                        ]
                    }
                }
            }
        }
    },
    {
        $project: {
            labor_of_love: {
                $gt: [
                    {$size: {$setIntersection: ["$cast", "$directors", "$writers"]}},
                    0
                ]
            }
        }
    },
    {
        $match: {labor_of_love: true}
    },
    {
        $count: "labors of love"
    }
])

Rozdział 2 - utility stages

$addFields

Działa nieco podobnie jak $project z tą różnicą, że definiuje nowe pola, nie usuwając żadnych pól w dokumencie wyjściowym.

$geoNear

Outputs documents in order of nearest to farthest from a specified point.

  1. Kolekcja może mieć tylko jeden indeks - 2dsphere
  2. Używając 2dsphere, dystans jest zwracany w metrach. Starsze koordynaty są zwracane w radianach.
  3. $geoNear musi być pierwszym stagem w aggregation pipeline.

Cursor-like stages

Funkcje kursorowe: skip, limit, sort - są dostępne nie tylko w przypadku find(), ale także jako stage's w aggregation pipeline.

W przypadku find możemy używać ich w następujący sposób:

// project fields ``numberOfMoons`` and ``name``
db.solarSystem.find({}, {"_id": 0, "name": 1, "numberOfMoons": 1}).pretty();

// count the number of documents
db.solarSystem.find({}, {"_id": 0, "name": 1, "numberOfMoons": 1}).count();

// skip documents
db.solarSystem.find({}, {
    "_id": 0,
    "name": 1,
    "numberOfMoons": 1
}).skip(5).pretty();

// limit documents
db.solarSystem.find({}, {
    "_id": 0,
    "name": 1,
    "numberOfMoons": 1
}).limit(5).pretty();

// sort documents
db.solarSystem.find({}, {
    "_id": 0,
    "name": 1,
    "numberOfMoons": 1
}).sort({"numberOfMoons": -1}).pretty();

Podobnie podczas agregacji używamy ich w następujący sposób:

// ``$limit`` stage
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "numberOfMoons": 1
    }
},
    {"$limit": 5}]).pretty();

// ``skip`` stage
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "numberOfMoons": 1
    }
}, {
    "$skip": 1
}]).pretty()

// ``$count`` stage
db.solarSystem.aggregate([{
    "$match": {
        "type": "Terrestrial planet"
    }
}, {
    "$project": {
        "_id": 0,
        "name": 1,
        "numberOfMoons": 1
    }
}, {
    "$count": "terrestrial planets"
}]).pretty();

// removing ``$project`` stage since it does not interfere with our count
db.solarSystem.aggregate([{
    "$match": {
        "type": "Terrestrial planet"
    }
}, {
    "$count": "terrestrial planets"
}]).pretty();


// ``$sort`` stage
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "numberOfMoons": 1
    }
}, {
    "$sort": {"numberOfMoons": -1}
}]).pretty();

// sorting on more than one field
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "hasMagneticField": 1,
        "numberOfMoons": 1
    }
}, {
    "$sort": {"hasMagneticField": -1, "numberOfMoons": -1}
}]).pretty();

// setting ``allowDiskUse`` option
db.solarSystem.aggregate([{
    "$project": {
        "_id": 0,
        "name": 1,
        "hasMagneticField": 1,
        "numberOfMoons": 1
    }
}, {
    "$sort": {"hasMagneticField": -1, "numberOfMoons": -1}
}], {"allowDiskUse": true}).pretty();

Podsumowując:

  • $sort, $skip, $limit i $count są stage'ami odpowiadającymi metodom kursorowym.
  • $sort może wykorzystać indeksy, jeśli jest wczesnym etapem w aggregation pipeline.
  • domyślnie $sort może używać do 100MB; ustawienie opcji allowDiskUse: true pozwala na sortowanie większych zbiorów (wykorzystując do tego pamięć trwałą).

$sample

Składnia: {$sample: {size: N}}

Wybiera losowo zbiór dokumentów z kolekcji na dwa sposoby:

  1. Z użyciem pseudolosowego kursora, jeżeli spełnione są warunki:
    • N nie przekracza 5% wszystkich dokumentów w kolekcji;
    • kolekcja źródłowa ma co najmniej 100 dokumentów;
    • $sample jest pierwszym stagem w aggregation pipeline;
  2. Bez kursora - sortując i wybierając pseudolosowo w pamięci operacyjnej.
    • Podlega tym samym ograniczeniom, co stage $sort (może używać do 100MB).

Lab: Using Cursor-like Stages

My answer:

db.getCollection('movies').aggregate([
    {
        $match: {
            "tomatoes.viewer.rating": {$gte: 3},
            countries: {$eq: "USA"},
            cast: {$elemMatch: {$exists: true}}
        }
    },
    {
        $addFields: {
            num_favs: {
                $size: {
                    $setIntersection: [
                        "$cast", [
                            "Sandra Bullock",
                            "Tom Hanks",
                            "Julia Roberts",
                            "Kevin Spacey",
                            "George Clooney"
                        ]
                    ]
                }
            }
        }
    },
    {
        $sort: {
            num_favs: -1,
            "tomatoes.viewer.rating": -1,
            title: -1
        }
    },
    {
        $skip: 24
    },
    {
        $limit: 1
    }
])

Correct answer:

var favorites = [
    "Sandra Bullock",
    "Tom Hanks",
    "Julia Roberts",
    "Kevin Spacey",
    "George Clooney"]

db.movies.aggregate([
    {
        $match: {
            "tomatoes.viewer.rating": {$gte: 3},
            countries: "USA",
            cast: {
                $in: favorites
            }
        }
    },
    {
        $project: {
            _id: 0,
            title: 1,
            "tomatoes.viewer.rating": 1,
            num_favs: {
                $size: {
                    $setIntersection: [
                        "$cast",
                        favorites
                    ]
                }
            }
        }
    },
    {
        $sort: {num_favs: -1, "tomatoes.viewer.rating": -1, title: -1}
    },
    {
        $skip: 24
    },
    {
        $limit: 1
    }
])

Z dokumentacji $in:

{ field: { $in: [value1, value2, ... valueN ] } }

The $in operator selects the documents where the value of a field equals any value in the specified array. To specify an $in expression, use the following prototype:

If the field holds an array, then the $in operator selects the documents whose field holds an array that contains at least one element that matches a value in the specified array (for example, value1, value2, and so on).

Lab - Bringing it all together

My answer:

var votes_max = 1521105,
    votes_min = 5,
    min = 1,
    max = 10;

// scaledVotes = min + (max - min) * (votes - votes_min) / (votes_max - votes_min)
db.movies.aggregate([
    {
        $match: {
            languages: "English",
            "imdb.rating": {$gte: 1},
            "imdb.votes": {$gte: 1},
            released: {$gte: ISODate("1990-01-01T00:00:00.000Z")}
        },
    }, {
        $addFields: {
            scaled_votes: {
                $add: [
                    min,
                    {
                        $multiply: [
                            (max - min),
                            {
                                $divide: [
                                    {$subtract: ["$imdb.votes", votes_min]},
                                    (votes_max - votes_min)
                                ]
                            }

                        ]
                    }
                ]
            }
        }
    },
    {
        $addFields: {
            normalized_rating: {
                $avg: [
                    "$scaled_votes",
                    "$imdb.rating"
                ]
            }
        }
    },
    {
        $sort: {
            normalized_rating: 1
        }
    },
    {
        $limit: 1
    },
    {
        $project: {
            _id: 0,
            title: 1
        }
    }
])

Correct answer:

db.movies.aggregate([
    {
        $match: {
            year: {$gte: 1990},
            languages: {$in: ["English"]},
            "imdb.votes": {$gte: 1},
            "imdb.rating": {$gte: 1}
        }
    },
    {
        $project: {
            _id: 0,
            title: 1,
            "imdb.rating": 1,
            "imdb.votes": 1,
            normalized_rating: {
                $avg: [
                    "$imdb.rating",
                    {
                        $add: [
                            1,
                            {
                                $multiply: [
                                    9,
                                    {
                                        $divide: [
                                            {$subtract: ["$imdb.votes", 5]},
                                            {$subtract: [1521105, 5]}
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        }
    },
    {$sort: {normalized_rating: 1}},
    {$limit: 1}
])

Rozdział 3: Core Aggregation - Combining Information

$group

Grupuje dokumenty po podanym _id. Składnia:

{
  $group:
    {
      _id: <expression>, // Group By Expression
      <field1>: { <accumulator1> : <expression1> },
      ...
    }
 }

Z dokumentacji:

_id: Required. If you specify an _id value of null, or any other constant value, the $group stage calculates accumulated values for all the input documents as a whole. See example of Group by Null.

field: Optional. Computed using the accumulator operators.

Warto zapamiętać:

  • _id może być wyrażeniem, nie tylko polem, po którym grupujemy dokumenty
  • możemy uzywać dowolnych wyrażeń akumulacyjnych wewnątrz $group
  • może zaistnieć potrzeba przygotowania danych (bo np. są nulle)

Przykład 1:

db.movies.aggregate([
    {
        $group: {
            _id: "$year",
            numOfDocs: {$sum: 1}
        }
    },
    {
        $sort: {numOfDocs: -1}
    }
])

Przykład 2:

db.movies.aggregate([
    {
        $match: {
            metacritic: {$gt: 0}
        }
    },
    {
        $group: {
            _id: {
                numDirectors: {
                    $cond: {
                        if: {$isArray: "$directors"},
                        then: {$size: "$directors"},
                        else: 0
                    }
                }
            },
            numFilms: {$sum: 1},
            avgMetacritics: {$avg: "$metacritic"}
        }
    },
    {
        $sort: {"id.numDirectors": -1}
    }
])

Z dokumentacji $cond:

Syntax:

{ $cond: { if: , then: , else: } }

{ $cond: [ , , ] }

Evaluates a boolean expression to return one of the two specified return expressions. If the evaluates to true, then $cond evaluates and returns the value of the expression. Otherwise, $cond evaluates and returns the value of the expression.

Accumulator stages with $project

Wyrażenia akumulacyjne w $project operują na tablicach tylko w obrębie bieżącego (pojedynczego) dokumentu, nie przenoszą w żaden sposób wartości ze wszystkich dokumentów.

$reduce

db.getCollection('icecream_data').aggregate([
    {
        $project: {
            _id: 0,
            max_high: {
                $reduce: {
                    input: "$trends",
                    initialValue: -Infinity,
                    in: {
                        $cond: [
                            {$gt: ["$$this.avg_high_tmp", "$$value"]},
                            "$$this.avg_high_tmp",
                            "$$value"
                        ]
                    }
                }
            }
        }
    }
])

Z dokumentacji $reduce:

Applies an expression to each element in an array and combines them into a single value. Składnia:

{
    $reduce: {
        input: <array>, // 
        initialValue: <expression>,
        in: <expression>
    }
}
  • input - wyrażenie ewaluujące się do tablicy; jeśli null -> $reduce zwróci null
  • initialValue - wartość początkowoa akumulatora, nadawana przed pierwszym odpaleniem in
  • in - wyrażenie, liczące nową wartość akumulatora; dostępne zmienne to:
    • $$this - aktualnie przetwarzany element tablicy
    • $$value - dotychczasowa wartość akumulatora

$max

Wyznacza największą wartość z tablicy, np.

db.getCollection('icecream_data').aggregate([
    {
        $project: {
            _id: 0,
            max_high: {
                $max: "$trends.avg_high_tmp"
            }
        }
    }
])

$min

Wyznacza najmniejszą wartość z tablicy, np.

db.getCollection('icecream_data').aggregate([
    {
        $project: {
            _id: 0,
            min_low: {
                $min: "$trends.avg_low_tmp"
            }
        }
    }
])

$avg, $stdDevPop

Średnia oraz odchylenie standardowe populacji:

db.getCollection('icecream_data').aggregate([
    {
        $project: {
            _id: 0,
            average_cpi: {$avg: "$trends.icecream_cpi"},
            cpi_deviation: {$stdDevPop: "$trends.icecream_cpi"}
        }
    }
])

$sum

Sumuje wyrażenia dla każdego elementu z tablicy:

db.getCollection('icecream_data').aggregate([
    {
        $project: {
            _id: 0,
            "yearly_sales (millions)": {
                $sum: "$trends.icecream_sales_in_millions"
            }
        }
    }
])

Zapamiętać, że:

  • Dostępne wyrażenia akumulacyjne:
    • $sum, $avg, $max, $min, $stdDevPop, $stdDevSamp
  • Wyrażenia nie dzielą pamięci pomiędzy dokumentami
  • Czasem trzeba użyć $map lub $reduce przy bardziej skomplikowanych obliczeniach.

Lab - $group and Accumulators

Moje rozwiązanie:

db.getCollection('movies').aggregate([
    {
        $match: {
            awards: {$regex: /Won\ [\d]+\ Oscars?/},
            "imdb.rating": {$gt: 0}
        }
    },
    {
        $group: {
            _id: null,
            highest_rating: {$max: "$imdb.rating"},
            lowest_rating: {$min: "$imdb.rating"},
            average_rating: {$avg: "$imdb.rating"},
            deviation: {$stdDevSamp: "$imdb.rating"}
        }
    },
    {$project: {_id: 0}}
])

$unwind

Wariant 1 (short form):

{ $unwind: <field path> }

Z dokumentacji:

You can pass the array field path to $unwind. When using this syntax, $unwind does not output a document if the field value is null, missing, or an empty array. Przykład:

db.getCollection('movies').aggregate([
    {
        $match: {
            "imdb.rating": {$gt: 0},
            year: {$gte: 2010, $lte: 2015},
            runtime: {$gte: 90}
        }
    },
    {
        $unwind: "$genres"
    },
    {
        $group: {
            _id: {
                year: "$year",
                genre: "$genres"
            },
            average_rating: {$avg: "$imdb.rating"}
        }
    },
    {
        $sort: {
            "_id.year": -1,
            average_rating: -1
        }
    },
    {
        $group: {
            _id: "$_id.year",
            genre: {$first: "$_id.genre"},
            average_rating: {$first: "$average_rating"}
        }
    },
    {
        $sort: {
            _id: -1
        }
    }
])

Wariant 2 (long form):

$unwind: {
    path: <field path>,
    includeArrayIndex: <string>,
    preserveNulllAndEmptyArrays: <boolean>
}

Warto zapamiętać:

  • $unwind działa tylko na wartościach tablicowych.
  • Są dwie formy: długa (obiekt) i krótka (nazwa pola).
  • Używanie $unwind na dużych kolekcjach z dużymi dokumentami może prowadzić do problemów z wydajnością.

Lab - $unwind

Moje rozwiązanie:

db.getCollection('movies').aggregate([
    {
        $unwind: "$cast"
    },
    {
        $group: {
            _id: "$cast",
            numFilms: {$sum: 1},
            average: {$avg: "$imdb.rating"}
        }
    },
    {
        $project: {
            _id: 1,
            numFilms: 1,
            average: {$trunc: ["$average", 1]}
        }
    }
])
db.getCollection('movies').aggregate([
    {
        $unwind: "$cast"
    },
    {
        $addFields: {
            isEnglish: {
                $size: {
                    $setIntersection: [
                        {
                            $cond: [{
                                $isArray: "$languages",
                            }, "$languages", []]
                        }, ["English"]
                    ]
                }
            }
        }
    },
    {
        $group: {
            _id: "$cast",
            numOfFilms: {$sum: 1},
            numOfEnglishFilms: {$sum: "$isEnglish"},
            average: {$avg: "$imdb.rating"}
        }
    },
    {
        $sort: {
            numOfEnglishFilms: -1
        }
    },
    {
        $project: {
            _id: 1,
            numOfFilms: 1,
            average: {$trunc: ["$average", 1]}
        }
    }
])

Correct answer:

db.movies.aggregate([
    {
        $match: {
            languages: "English"
        }
    },
    {
        $project: {_id: 0, cast: 1, "imdb.rating": 1}
    },
    {
        $unwind: "$cast"
    },
    {
        $group: {
            _id: "$cast",
            numFilms: {$sum: 1},
            average: {$avg: "$imdb.rating"}
        }
    },
    {
        $project: {
            numFilms: 1,
            average: {
                $divide: [{$trunc: {$multiply: ["$average", 10]}}, 10]
            }
        }
    },
    {
        $sort: {numFilms: -1}
    },
    {
        $limit: 1
    }
])

$lookup

Tworzy połączenie typu left-outer join między kolekcjami. Składnia:

$lookup: {
    from: <collection to join>,
    localField: <field from the input documents>,
    foreignField: <field from the documents of the "from" collection>,
    as: <output array field>
}

from - kolekcją, którą dołączamy localField - może być pojedynczą wartością lub tablicą, foreignField - może być pojedynczą wartością lub tablicą.

$lookup posługuje się operatorem === (strict equality comparison) do porównywania dokumentów.

Przykład:

db.getCollection('air_alliances').aggregate([
    {
        $lookup: {
            from: "air_airlines",
            localField: "airlines",
            foreignField: "name",
            as: "airlines"
        }
    }
])

Zapamiętać, że:

  1. kolekcja from nie może być sharded (podzielona między węzły w klastrze),
  2. kolekcja from musi być w tej samej bazie danych,
  3. Wartości w localField i foreignField są dopasowywane wg równości,
  4. as może być dowolną nazwą, ale jeśli w dokumentach źródłowych istnieje pole o takiej nazwie, to zostanie ono nadpisane tablicą wynikową.

Lab - Using $lookup

Moje rozwiązanie:

db.getCollection('air_routes').aggregate([
    {
        $match: {
            airplane: /747|380/
        }
    },
    {
        $lookup: {
            from: "air_alliances",
            localField: "airline.name",
            foreignField: "airlines",
            as: "alliances"
        }
    },
    {$unwind: "$alliances"},
    {
        $project: {
            airplane: 1,
            alliance: "$alliances.name"
        }
    },
    {
        $group: {
            _id: "$alliance",
            total: {$sum: 1}
        }
    },
    {
        $sort: {
            total: -1
        }
    }
])


db.getCollection('air_alliances').aggregate([
    {
        $lookup: {
            from: "air_routes",
            localField: "airlines",
            foreignField: "airline.name",
            as: "routes"
        }
    },
    {
        $unwind: "$routes"
    },
    {
        $project: {
            name: 1,
            airplane: "$routes.airplane"
        }
    },
    {
        $match: {
            airplane: /747|380/
        }
    },
    {
        $group: {
            _id: "$name",
            numRoutes: {$sum: 1}
        }
    },
    {
        $sort: {
            numRoutes: -1
        }
    }
])

$graphLookup

Umożliwia rekursywne oepracje na dokumentach.

Składnia:

$graphLookup: {
    from: <lookup table>,
    startWith: <expression for value to start from>,
    connectFromField: <field name to connect from>,
    connectToField: <field name to connect to>,
    as: <field name for result array>,
    maxDepth: <number of iterations to perform>,
    depthField: <field name for number of iterations to reach this node>,
    restrictSearchWithMatch: <match condition to apply to lookup>
}

Przykład:

Hierarchia w dół (podwładni):

db.getCollection('parent_reference').aggregate([
    {
        $match: {
            name: "Eliot",
        }
    },
    {
        $graphLookup: {
            from: "parent_reference",
            startWith: "$_id",
            connectFromField: "_id",
            connectToField: "reports_to",
            as: "all_reports"
        }
    }
])

Hierarchia w górę (szefowie):

db.getCollection('parent_reference').aggregate([
    {
        $match: {
            name: "Shannon",
        }
    },
    {
        $graphLookup: {
            from: "parent_reference",
            startWith: "$reports_to",
            connectFromField: "reports_to",
            connectToField: "_id",
            as: "bosses"
        }
    }
])

Odwrócona relacja (referencje do dzieci)

db.getCollection('child_reference').aggregate([
    {
        $match: {
            name: "Dev",
        }
    },
    {
        $graphLookup: {
            from: "child_reference",
            startWith: "$direct_reports",
            connectFromField: "direct_reports",
            connectToField: "name",
            as: "all_reports"
        }
    }
])

Sterowanie głębokością rekurencji

Parametr maxDepth - poziom rekurencji; 0 - tylko wartość początkowa (startWith). Parametr depthField - pole w wyniku, do którego wpisany jest poziom zagnieżdżenia rekurencyjneg (long). `

db.getCollection('child_reference').aggregate([
    {
        $match: {
            name: "Dev",
        }
    },
    {
        $graphLookup: {
            from: "child_reference",
            startWith: "$direct_reports",
            connectFromField: "direct_reports",
            connectToField: "name",
            as: "till_2_level_reports",
            maxDepth: 0,
            depthField: "level"
        }
    }
])

Cross Collection Lookup

Przykład:

db.airlines.aggregate([
    {$match: {name: "TAP Portugal"}},
    {
        $graphLookup: {
            from: "air_routes",
            as: "chain",
            startWith: "$base",
            connectFromField: "src_airport",
            connectToField: "dst_airport",
            maxDepth: 1
        }
    }
])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment