#mongodb #mongodb-query #time-series #aggregation-framework
#mongodb #mongodb-запрос #временные ряды #структура агрегации
Вопрос:
Мне интересно, возможно ли следующее в MongoDB.
У меня есть коллекция документов, которые представляют изменения в некотором значении во времени:
{
"day" : ISODate("2018-12-31T23:00:00.000Z"),
"value": [some integer value]
}
В данных нет «дыр», у меня есть записи за все дни в течение некоторого периода.
Можно ли запросить эту коллекцию, чтобы получить только документы, значение которых отличается от предыдущего (при сортировке по дате asc)? Например, наличие следующих документов:
{ day: ISODate("2019-04-01T00:00:00.000Z"), value: 10 }
{ day: ISODate("2019-04-02T00:00:00.000Z"), value: 10 }
{ day: ISODate("2019-04-03T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-04T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-05T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-06T00:00:00.000Z"), value: 10 }
Я хочу получить документы для 2018-04-01
2018-04-03
2018-04-06
и только для тех, поскольку другие не имеют изменения значения.
Ответ №1:
Вам нужно получить пары последовательных документов, чтобы обнаружить разрыв. Для этого вы можете поместить все документы в один массив и заархивировать его, сдвинув 1 элемент из заголовка:
db.collection.aggregate([
{ $sort: { day: 1 } },
{ $group: { _id: null, docs: { $push: "$$ROOT" } } },
{ $project: {
pair: { $zip: {
inputs:[ { $concatArrays: [ [false], "$docs" ] }, "$docs" ]
} }
} },
{ $unwind: "$pair" },
{ $project: {
prev: { $arrayElemAt: [ "$pair", 0 ] },
next: { $arrayElemAt: [ "$pair", 1 ] }
} },
{ $match: {
$expr: { $ne: ["$prev.value", "$next.value"] }
} },
{ $replaceRoot:{ newRoot: "$next" } }
])
Остальное тривиально — вы разматываете массив обратно в документы, сравниваете пары, отфильтровываете равные и заменяете ROOT из того, что осталось.
Комментарии:
1. Большое спасибо за этот ответ. Если вы не возражаете, у меня есть один дополнительный вопрос: можно ли добавить к этим данным группировку по какому-либо другому полю (например, userId)? Я думал, что мне удастся сделать это самостоятельно, но у меня возникли некоторые проблемы.
2. Но, конечно. Если я правильно понял вопрос, вам нужно только заменить
$group: {_id: null
на$group: {_id: "$userId"
. Если это не то, что вам нужно, пожалуйста, задайте правильный вопрос с формальными фрагментами данных и ожидаемыми результатами.
Ответ №2:
Начиная с Mongo 5
, это идеальный вариант использования для нового $setWindowFields
оператора агрегации:
// { day: ISODate("2019-04-01T00:00:00.000Z"), value: 10 } <=
// { day: ISODate("2019-04-02T00:00:00.000Z"), value: 10 }
// { day: ISODate("2019-04-03T00:00:00.000Z"), value: 15 } <=
// { day: ISODate("2019-04-04T00:00:00.000Z"), value: 15 }
// { day: ISODate("2019-04-05T00:00:00.000Z"), value: 15 }
// { day: ISODate("2019-04-06T00:00:00.000Z"), value: 10 } <=
db.collection.aggregate([
{ $setWindowFields: {
sortBy: { day: 1 },
output: { pair: { $push: "$value", window: { documents: [-1, "current"] } } }
}},
// { day: ISODate("2019-04-01T00:00:00Z"), value: 10, pair: [ 10 ] }
// { day: ISODate("2019-04-02T00:00:00Z"), value: 10, pair: [ 10, 10 ] }
// { day: ISODate("2019-04-03T00:00:00Z"), value: 15, pair: [ 10, 15 ] }
// { day: ISODate("2019-04-04T00:00:00Z"), value: 15, pair: [ 15, 15 ] }
// { day: ISODate("2019-04-05T00:00:00Z"), value: 15, pair: [ 15, 15 ] }
// { day: ISODate("2019-04-06T00:00:00Z"), value: 10, pair: [ 15, 10 ] }
{ $match: { $expr: { $or: [
{ $eq: [ { $size: "$pair" }, 1 ] }, // first doc doesn't have a previous doc
{ $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }
]}}},
{ $unset: ["pair"] }
])
// { day: ISODate("2019-04-01T00:00:00Z"), value: 10 }
// { day: ISODate("2019-04-03T00:00:00Z"), value: 15 }
// { day: ISODate("2019-04-06T00:00:00Z"), value: 10 }
Это:
- начинается со
$setWindowFields
стадии агрегирования, которая добавляетpair
поле, представляющее значение текущего документа и значение предыдущего документа (output: { pair: { ... }}
):$setWindowFields
предоставляет для данного документа представление других документов (awindow
)- который в нашем случае является
"current"
документом и предыдущим"-1"
:window: { documents: [-1, "current"] }
. - таким образом, мы создаем в этом окне массив значений:
$push: "$value"
- и обратите внимание, что мы позаботились о сортировке документов по дням :
sortBy: { day: 1 }
.
- который в нашем случае является
- и затем:
- фильтры в первом документе (который можно отметить по его массиву, имеющему только один элемент):
{ $eq: [ { $size: "$pair" }, 1 ] }
- и отфильтровывает следующие документы, если они
pair
имеют одинаковые значения:{ $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }
- фильтры в первом документе (который можно отметить по его массиву, имеющему только один элемент):