В MongoDB 2.1 была представлена структура агрегирования , более быстрая альтернатива Map / Reduce для общих операций агрегирования. Если вы взглянули на документацию и примеры, вы, возможно, обнаружили пугающую функцию. Как только вы приручите его, эта новая особенность проявит себя как очень могущественное животное. Так что читайте дальше, чтобы узнать его истинную силу с помощью ряда примеров.
Краткое введение о синтаксисе конвейера
Агрегация MongoDB — это серия специальных операторов, применяемых к коллекции. Оператор является объектом JavaScript с одним свойством, именем оператора, значением которого является объектом варианта:
{ $name: { /* options */ } }
Поддерживаемые имена оператора являются: $project
, $match
, $limit
, $skip
, $unwind
, $group
, и $sort
, каждый со своим собственным набором опций. Ряд операторов называется конвейером :
[{ $project: { /* options */ } }, { $match: { /* options */ } }, { $group: { /* options */ } }]
При выполнении конвейера MongoDB направляет операторов друг к другу. «Труба» здесь имеет значение для Linux: вывод оператора становится входом следующего оператора. Результатом каждого оператора является новая коллекция документов. Таким образом, Mongo выполняет предыдущий конвейер следующим образом:
collection | $project | $match | $group => result
Вы можете добавить в конвейер столько операторов, сколько вам нужно, даже в два раза больше, в разных позициях:
collection | $match | $group | $match | $project | $group => result
Это объясняет, почему конвейер написан не как простой объект JavaScript, а как набор объектов: в объекте один и тот же оператор не может появляться дважды:
// The first appearance of $match and $group would be ignored with this syntax { $match: { /* options */ }, $group: { /* options */ }, $match: { /* options */ }, $project: { /* options */ }, $group: { /* options */ } } // So MongoDB imposes a collection of JavaScript objects instead [ { $match: { /* options */ } }, { $group: { /* options */ } }, { $match: { /* options */ } }, { $project: { /* options */ } }, { $group: { /* options */ } } ] // That's longer and cumbersome to read, but you'll get used to it
Чтобы выполнить конвейер для коллекции MongoDB, используйте aggregate()
функцию этой коллекции:
db.books.aggregate([{ $project: { title: 1 } }]);
Совет: Если вы используете Node.js, как собственный адаптер (начиная с версии 0.9.9.2), так и ODM (начиная с версии 3.1.0) поддерживают новую структуру агрегирования. Например, чтобы выполнить предыдущий конвейер на модели Mongoose, вам просто нужно написать:
Books.aggregate([{ $project: { title: 1 } }], function(err, results) { // do something with the result });
Основное преимущество инфраструктуры агрегации состоит в том, что MongoDB выполняет ее без дополнительных затрат на движок JavaScript. Это реализовано непосредственно в C ++, и поэтому это очень быстро. Основное ограничение по сравнению с классическим агрегированием SQL — это то, что оно ограничено одной коллекцией. Другими словами, вы не можете выполнить агрегацию Монго для нескольких коллекций, используя операцию, подобную JOIN. Кроме того, это очень мощный.
В этом посте я проиллюстрирую мощь операторов конвейера на примере и сравню их с аналогами SQL. Для получения подробной информации перейдите на docs.mongodb.org .
Выберите, псевдоним, составить
Используйте $project
оператор для выбора или переименования свойств из коллекции — аналогично тому, как вы делали бы с предложением SQL SELECT.
/ sample data > db.books.find(); [ { _id: 147, title: "War and Peace", ISBN: 9780307266934 }, { _id: 148, title: "Anna Karenina", ISBN: 9781593080273 }, { _id: 149, title: "Pride and Prejudice", ISBN: 9783526419358 }, ] # sample data > SELECT * FROM book; +-----+-----------------------+---------------+ | id | title | ISBN | +-----+-----------------------+---------------+ | 147 | 'War and Peace' | 9780307266934 | | 148 | 'Anna Karenina' | 9781593080273 | | 149 | 'Pride and Prejudice' | 9783526419358 | +-----+-----------------------+---------------+ > db.books.aggregate([ { $project: { title: 0, // eliminate from the output reference: "$ISBN" // use ISBN as source } } ]); [ { _id: 147, reference: 9780307266934 }, { _id: 148, reference: 9781593080273 }, { _id: 149, reference: 9783526419358 }, ] > SELECT id, ISBN AS reference FROM book; +-----+---------------+ | id | reference | +-----+---------------+ | 147 | 9780307266934 | | 148 | 9781593080273 | | 149 | 9783526419358 | +-----+---------------+
$project
Оператор также может создавать составленные поля и вложенные документы , используя любой из поддерживаемых операторов выражения ( $and, $or, $gt, $lt, $eq, $add, $mod, $substr, $toLower, $toUpper, $dayOfWeek, $hour, $cond, $ifNull
, чтобы назвать несколько).
Группировка документов
Группируйте документы с, как вы уже догадались, $group
оператором.
// fastest way > db.books.count(); 3 // if you really want to use aggregation > db.books.aggregate([ { $group: { // _id is required, so give it a constant value // to group all the collection into one result _id: null, // increment nbBooks for each document nbBooks: { $sum: 1 } } } ]); [ { _id: null, nbBooks: 3 } ] > SELECT COUNT(*) FROM book; +----------+ | COUNT(*) | +----------+ | 3 | +----------+ // sample data > db.books.find() [ { _id: 147, title: "War and Peace", author_id: 72347 }, { _id: 148, title: "Anna Karenina", author_id: 72347 }, { _id: 149, title: "Pride and Prejudice", author_id: 42345 } ] # sample data > SELECT * FROM book +-----+---------------------+-----------+ | id | title | author_id | +-----+---------------------+-----------+ | 147 | War and Peace | 72347 | | 148 | Anna Karenina | 72347 | | 149 | Pride and Prejudice | 42345 | +-----+---------------------+-----------+ > db.books.aggregate([ { $group: { // group by author_id _id: "$author_id", // increment nbBooks for each document nbBooks: { $sum: 1 } } } ]); [ { _id: 72347, nbBooks: 2 }, { _id: 42345, nbBooks: 1 } ] > SELECT author_id, COUNT(*) FROM book GROUP BY author_id; +-----------+----------+ | author_id | COUNT(*) | +-----------+----------+ | 72347 | 2 | | 42345 | 1 | +-----------+----------+
Мультиоператорский конвейер
В конвейере может быть несколько операторов. Вот комбинация $group
и $project
:
> db.books.aggregate([ { $group: { _id: "$author_id", nbBooks: { $sum: 1 } } }, { $project: { _id: 0, authorId: "$_id", nbBooks: 1 } } ]); [ { authorId: 72347, nbBooks: 2 }, { authorId: 42345, nbBooks: 1 } ] > SELECT author_id AS author, COUNT(*) AS nb_books FROM book GROUP BY author_id; +--------+----------+ | author | nb_books | +--------+----------+ | 72347 | 2 | | 42345 | 1 | +--------+----------+
Более сложные агрегаты
$group
поддерживает множество функций агрегирования:, $first, $last, $min, $max, $avg, $sum, $push
и $addToSet
. Проверьте документацию MongoDB для полной справки.
// sample data > db.reviews.find(); [ { _id: "455", bookId: "974147", date: new Date("2012-07-10"), score: 1 }, { _id: "456", bookId: "345335", date: new Date("2012-07-12"), score: 5 }, { _id: "457", bookId: "345335", date: new Date("2012-07-13"), score: 2 }, { _id: "458", bookId: "974147", date: new Date("2012-07-16"), score: 3 } ] # sample data > SELECT * FROM review; +-----+---------+--------------+-------+ | id | book_id | date | score | +-----+---------+--------------+-------+ | 455 | 974147 | "2012-07-10" | 1 | | 456 | 345335 | "2012-07-12" | 5 | | 457 | 345335 | "2012-07-13" | 2 | | 458 | 974147 | "2012-07-16" | 3 | +-----+---------+--------------+-------+ > db.reviews.aggregate([ { $group: { _id: "$bookId", avgScore: { $avg: "$score" }, maxScore: { $max: "$score" }, nbReviews: { $sum: 1 } } } ]); [ { _id: 345335, avgScore: 3.5, maxScore: 5, nbReviews: 2 }, { _id: 974147, avgScore: 3, maxScore: 3, nbReviews: 2 } ] > SELECT book_id, AVG(score) as avg_score, MAX(score) as max_score, COUNT(*) as nb_reviews FROM review GROUP BY book_id ; +---------+------------+----------+------------+ | book_id | avg_score | max_score | nb_reviews | +---------+------------+----------+------------+ | 345335 | 3.5 | 5 | 2 | | 974147 | 2 | 3 | 2 | +---------+------------+----------+------------+
условия
Вы можете ограничить обработку коллекции, используя объект запроса , передаваемый $match
оператору. Размещаете ли вы этот оператор перед или после $group
оператора, он становится эквивалентом WHERE
или HAVING
в SQL.
> db.reviews.aggregate([ { $match : { date: { $gte: new Date("2012-07-11") } } }, { $group: { _id: "$bookId", avgScore: { $avg: "$score" } } } ]); [ { _id: 345335, avgScore: 3.5 }, { _id: 974147, avgScore: 3 } ] > SELECT book_id, AVG(score) FROM review WHERE review.date > "2012-07-11" GROUP BY review.book_id ; +---------+------------+ | book_id | AVG(score) | +---------+------------+ | 345335 | 3.5 | | 974147 | 3 | +---------+------------+ > db.reviews.aggregate([ { $group: { _id: "$bookId", avgScore: { $avg: "$score" } } }, { $match : { avgScore: { $gt: 3 } } } ]); [ { _id: 345335, avgScore: 3.5 } ] > SELECT book_id, AVG(score) AS avg_score FROM review GROUP BY review.book_id HAVING avg_score > 3; +---------+------------+ | book_id | AVG(score) | +---------+------------+ | 345335 | 3.5 | +---------+------------+
Разработка встроенных массивов
Если документы внутри коллекции содержат массивы, вы можете разворачивать («раскручивать») эти массивы в несколько уникальных документов с помощью $unwind
оператора.
// sample data > db.articles.find(); [ { _id: 12351254, title: "Space Is Getting Closer", tags: ["science", "space", "iss"] }, { _id: 22956492, title: "Computer Solves Rubiks Cube", tags: ["computing", "science"] } ] # sample data > SELECT * FROM article; +------------+---------------------------+ | id | title | +----------+-----------------------------+ | 12351254 | Space Is Getting Closer | | 22956492 | Computer Solves Rubiks Cube | +------------+---------------------------+ > SELECT * FROM tag; +-----+------------+-----------+ | id | article_id | name | +-----+------------+-----------+ | 534 | 12351254 | science | | 535 | 12351254 | space | | 536 | 12351254 | iss | | 816 | 22956492 | computing | | 817 | 22956492 | science | +-----+------------+-----------+ > db.articles.aggregate([ { $unwind: "$tags" } ]); [ { _id: 12351254, title: "Space Is Getting Closer", tags: "science" }, { _id: 12351254, title: "Space Is Getting Closer", tags: "space" }, { _id: 22956492, title: "Computer Solves Rubiks Cube", tags: "computing" }, { _id: 22956492, title: "Computer Solves Rubiks Cube", tags: "science" } ] > SELECT article.id, article.title, tag.name FROM article LEFT JOIN tag ON article.id = tag.article_id; +------------+-----------------------------+-----------+ | article.id | article.title | tag.name | +------------+-----------------------------+-----------+ | 12351254 | Space Is Getting Closer | science | | 12351254 | Space Is Getting Closer | space | | 22956492 | Computer Solves Rubiks Cube | computing | | 22956492 | Computer Solves Rubiks Cube | science | +------------+-----------------------------+-----------+
Совокупные развитые массивы
Истинная сила структуры агрегации раскрывается, когда вы переходите $unwind
на $group
. Это похоже на использование LEFT JOIN ... GROUP BY
в SQL.
> db.articles.aggregate([ { $unwind: "$tags" }, { $group: { _id: "$tags", nbArticles: { $sum: 1 } } } ]); [ { _id: "science", nbArticles: 2 }, { _id: "space", nbArticles: 1 }, { _id: "computing", nbArticles: 1 }, ] > SELECT tag.name, COUNT(article.id) AS nb_articles FROM article LEFT JOIN tag ON article.id = tag.article_id GROUP BY tag.name; +-----------+-------------+ | tqg.name | nb_articles | +-----------+-------------+ | science | 2 | | space | 1 | | computing | 1 | +-------------+-----------+ > db.articles.aggregate([ { $unwind: "$tags" }, { $group: { _id: "$tags", articles: { $addToSet: "$_id" } } } ]); [ { _id: "science", articles: [12351254, 22956492] }, { _id: "space", articles: [12351254] }, { _id: "computing", articles: [22956492] }, ] > SELECT tag.name, GROUP_CONCAT(article.id) AS articles FROM article LEFT JOIN tag ON article.id = tag.article_id GROUP BY tag.name; +-----------+-------------------+ | tqg.name | articles | +-----------+-------------------+ | science | 12351254,22956492 | | space | 12351254 | | computing | 22956492 | +-------------+-----------------+
Вывод
Представьте, что вы можете сделать с этой системой … Операторы конвейера один за другим, группировать, сортировать, ограничивать и т. Д. В конечном итоге, взятом из самой документации MongoDB , показан конвейер с двумя последовательными $group
операторами. База данных SQL может делать это только с подзапросами.
Если ваши функции Map / Reduce достаточно просты, выполните рефакторинг кода Mongo для новой структуры агрегирования. Он будет выполняться быстрее и откроется для нового царства возможностей.