Статьи

Сравнение MongoDB New Aggregation Framework и SQL

В MongoDB 2.1 была представлена ​​структура агрегирования , более быстрая альтернатива Map / Reduce для общих операций агрегирования. Если вы взглянули на документацию и примеры, вы, возможно, обнаружили пугающую функцию. Как только вы приручите его, эта новая особенность проявит себя как очень могущественное животное. Так что читайте дальше, чтобы узнать его истинную силу с помощью ряда примеров.MongoDB Новая структура агрегации и SQL бок о бок

Краткое введение о синтаксисе конвейера

Агрегация 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 для новой структуры агрегирования. Он будет выполняться быстрее и откроется для нового царства возможностей.