Для обработки данных в коллекциях в MongoDB есть замечательный инструмент - map-reduce. Посмотрим, как он работает.
Я это делал на реальном примере по одному проекту - нужно обработать данные по пользовательским покупкам, которые хранятся в монге. Нас интересует два поля: ID пользователя и сумма покупки. В качестве ID пользователя выступает GUID.
Нужно сделать следующий отчет: показать пользователей с наибольшим количеством потраченных денег.
Если бы мы были в SQL, то табличка с данными о покупках была бы следующие:
CREATE TABLE sales(
guid varchar(32) NOT NULL PRIMARY KEY,
price float NOT NULL
);
Запрос, который выводит данный отчет, был бы следующий:
SELECT guid, sum(price) AS summa FROM sales
GROUP BY guid
ORDER BY summa DESC;
В монге данные хранятся в коллекции
stat, вид данных следующий:
> db.stat.findOne();
{
"_id" : ObjectId("4e4c73eb41e5c790a7391848"),
"guid" : "346fbb3968c9453d9dc8f8ebe0fa6763",
"item" : "Apple MacBook Air MC968LL/A 11.6-Inch",
"price" : 78.64,
"retailerId" : "COST",
"ip" : "0:0:0:0:0:0:0:1",
"datetime" : "2011-07-28 10:12:38",
"timezone" : "-1",
"createDate" : "Thu Aug 18 2011 06:07:39 GMT+0400 (MSK)",
}
Что ж, приступим в мап-редьюсу...
function salesMap() {
emit(this.guid, {price: this.price });
}
function salesReduce(key, values) {
var result = {
count: 0,
summa: 0.0
};
values.forEach(function(v){
if (v.price) {
result.summa += v.price;
result.count++;
}
});
return result;
}
db.userrep.drop();
db.stat.mapReduce(salesMap,salesReduce,{out:'userrep', verbose: true});
db.userrep.find();
Функция
emit(key, value), которую мы использовали в функции
salesMap, подает на вход редьюсу пару ключ - значение. Ключом мы выбрали GUID, т.к. по нему идет группировка, а в значение кладем цену товара.
Функция
salesReduce получает на вход набор объектов, сгруппированных по ключу.
Внутри функции все тривиально:
1) создаем объект с результатами, пока что содержащий нулевые значения var result = { count: 0, summa: 0.0 };
2) пробегаемся по коллекции и заполняем результат
3) возвращаем результат.
4) PROFIT!!!
Вид результатов следующий:
> db.userrep.find().limit(5)
{ "_id" : "01dc299862094450ac232d384d883a5f", "value" : { "count" : 2, "summa" : 245.19 } }
{ "_id" : "0a1afef3b8b27942d9a8d02903ca2c28", "value" : { "count" : 1, "summa" : 30.43 } }
{ "_id" : "0c9d6a05458313b85706548c290991e9", "value" : { "count" : 0, "summa" : 0 } }
{ "_id" : "0f4e595202884408ae4c4c6306d764f9", "value" : { "count" : 1, "summa" : 88.43 } }
{ "_id" : "17a57229a9c14b2cb46337a3d196051c", "value" : { "count" : 1, "summa" : 49.54 } }
Какая жалость - данные не отсортированы.
Монго не умеет сортировать коллекцию данным из составных объектов, т.е. мы не можем написать что-то вроде:
db.userrep.find().sort({value.summa: -1});
Поэтому придется немножко извратиться:
db.userrep.find().forEach(function(v) {
var s = v.value.summa;
var c = v.value.count;
var id = v._id;
db.userrep.update({_id: id},{$set:{summa:s, count: c}}, true, true);
});
Мы просто скопировали поля
summa, count из объекта
value в объект-контейнер коллекции.
Теперь данные выглядят так:
> db.userrep.find().limit(5);
{ "_id" : "01dc299862094450ac232d384d883a5f", "count" : 2, "summa" : 245.19, "value" : { "count" : 2, "summa" : 245.19 } }
{ "_id" : "0a1afef3b8b27942d9a8d02903ca2c28", "count" : 1, "summa" : 30.43, "value" : { "count" : 1, "summa" : 30.43 } }
{ "_id" : "0c9d6a05458313b85706548c290991e9", "count" : 0, "summa" : 0, "value" : { "count" : 0, "summa" : 0 } }
{ "_id" : "0f4e595202884408ae4c4c6306d764f9", "count" : 1, "summa" : 88.43, "value" : { "count" : 1, "summa" : 88.43 } }
{ "_id" : "17a57229a9c14b2cb46337a3d196051c", "count" : 1, "summa" : 49.54, "value" : { "count" : 1, "summa" : 49.54 } }
Теперь, наконец, можно получить отсортированные данные:
> db.userrep.find().sort({summa:-1}).limit(5);
{ "_id" : "cb1332aed05d48fd9c51d0c5be584156", "count" : 4, "summa" : 496.72999999999996, "value" : { "count" : 4, "summa" : 496.72999999999996 } }
{ "_id" : "ade1efffe9d3fb116f33c320bf9b447e", "count" : 3, "summa" : 412.45000000000005, "value" : { "count" : 3, "summa" : 412.45000000000005 } }
{ "_id" : "ef98c2c430c4486394bc2b85ec5b1a77", "count" : 3, "summa" : 408.5, "value" : { "count" : 3, "summa" : 408.5 } }
{ "_id" : "5295f9b927c59dacdd3fb20d8173a19b", "count" : 4, "summa" : 367.08, "value" : { "count" : 4, "summa" : 367.08 } }
{ "_id" : "01dc299862094450ac232d384d883a5f", "count" : 2, "summa" : 245.19, "value" : { "count" : 2, "summa" : 245.19 } }