Суммарный агрегат Mongodb для нескольких диапазонов дат

В моей совокупности каждый документ в потоке будет иметь дату на нем.

Мне нужно суммировать некоторые значения в диапазонах дат.

Т.е.

{
    value: 3,
    date: [SoME TIME STAMP]
},
{
    value: 4,
    date: [SoME TIME STAMP]
},
{
    value: 1,
    date: [SoME TIME STAMP]
},
{
    value: -6,
    date: [SoME TIME STAMP]
}

Я хочу иметь возможность группировать эту базу документов в диапазоне дат. IE: 1-7 дней назад, 8-15 дней назад. и 15-30 дней назад.

db.Collection.aggregate([
{$match: {some matching}},
{$group: {What should i do here??}}
])

Я могу, конечно, сделать 3 разных агрегата с 3 разными $match в датах.

Возможно ли выполнить всю группу $и суммировать поле "значение" за один проход?

Ответ 1

Вам необходимо условно определить ключ группировки на основе того, где текущая дата находится между диапазонами. В основном это достигается с помощью $cond с вложенными условиями и логическим вариантом $lt:

// work out dates somehow
var today = new Date(),
    oneDay = ( 1000 * 60 * 60 * 24 ),
    thirtyDays = new Date( today.valueOf() - ( 30 * oneDay ) ),
    fifteenDays = new Date( today.valueOf() - ( 15 * oneDay ) ),
    sevenDays = new Date( today.valueOf() - ( 7 * oneDay ) );

db.collection.aggregate([
    { "$match": {
        "date": { "$gte": thirtyDays }
    }},
    { "$group": {
        "_id": {
            "$cond": [
                { "$lt": [ "$date", fifteenDays ] },
                "16-30",
                { "$cond": [
                    { "$lt": [ "$date", sevenDays ] },
                    "08-15",
                    "01-07"
                ]}
            ]
        },
        "count": { "$sum": 1 },
        "totalValue": { "$sum": "$value" }
    }}
])

Так как $cond является троичным оператором, первое условие оценивается, чтобы увидеть, является ли условие истинным, и когда оно истинно, возвращается второй аргумент, в противном случае третий возвращается, когда ложно. Таким образом, вложив другой $cond в ложный случай, вы получите логический тест на то, куда попадает дата: либо "меньше 15-дневной даты", что означает, что она находится в самом старом диапазоне, либо "менее 7 дней", что означает средний диапазон, или, конечно, в новейшем диапазоне.

Я просто добавляю здесь цифры, меньшие 10, к 0, так что он дает вам возможность сортировать, если хотите, поскольку вывод "ключей" в $group не упорядочен сам по себе.

Но это то, как вы делаете это в одном запросе. Вы просто решаете, какой ключ группировки должен быть основан на том, куда попадает дата, и накапливаете для каждого ключа.

Ответ 2

Первым шагом будет создание объектов даты, которые представляют ваш диапазон. Допустим, вы хотите запустить операцию агрегирования для перехода 8-15 дней назад, это означает, что вам нужны два объекта даты, скажем, начало и конец. start будет содержать дату днем ранее, а end - 8 дней назад. Создать эти объекты даты легко, если установить для них число дней, предшествовавших вычитанию n из даты, где n - это число дней назад:

var start = new Date();
start.setDate(start.getDate() - 8);

var end = new Date();
end.setDate(end.getDate() - 15);

или вычитание из отметки времени в миллисекундах с использованием метода .getTime() возвращает стандартную отметку времени JavaScript (в миллисекундах с момента Jan 1/1970), в которой вы можете использовать обычные математические операции, и напрямую передается объекту Date:

var today = new Date();
var start = new Date(today.getTime() - 8*24*60*60*1000);
var end = new Date(today.getTime() - 15*24*60*60*1000);

Теперь, когда у вас есть объекты даты, вы можете использовать их в качестве $match критериев, используя $lte и $gte операторы сравнения:

var pipeline = [
    {
        "$match": {
            "date": { "$gte": start, "$lte": end }
        }
    }
]

Выполнение агрегации на этом этапе даст вам все документы, дата которых находится в диапазоне 8-15 дней назад,

db.aggregate(pipeline);

что эквивалентно запросу find() :

db.collection.find({
    "date": { "$gte": start, "$lte": end }
});

Теперь, на следующем этапе конвейера, вам нужно будет создать операцию агрегирования, которая задает группу _id со значением NULL, вычисляя общее значение и счетчики для всех документов в коллекции, используя $sum оператор аккумулятора:

var pipeline = [
    {
        "$match": {
            "date": { "$gte": start, "$lte": end }
        }
    },
    {
        "$group": {
            "_id": null,
            "totalValues": { "$sum": "$value" },
            "count": { "$sum": 1 }
        }
    }
]

db.collection.aggregate(pipeline);

Вы можете даже пойти дальше, чтобы создать универсальную функцию, которая возвращает фактическую сумму из вышеуказанной операции агрегирования, которая принимает два параметра: начальное значение диапазона дат и конец:

var getTotalValues = function(start, end){
    var today = new Date();
    var startDate = new Date(today.getTime() - start*24*60*60*1000);
    var endDate = new Date(today.getTime() - end*24*60*60*1000);    

    var pipeline = [
            {
                "$match": {
                    "timestamp": { "$gte": startDate, "$lte": endDate }
                }
            },
            {
                "$group": {
                    "_id": null,
                    "totalValues": { "$sum": "$value" },            
                    "count": { "$sum": 1 }
                }
            }
        ],
        resultArray = db.collection.aggregate(pipeline).toArray();

    return resultArray[0].totalValues;
}

var total = getTotalValues(1, 8);
printjson(total); // prints the total