Запрос mongo для обнаружения изменений значений во временных рядах

#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 предоставляет для данного документа представление других документов (a window )
      • который в нашем случае является "current" документом и предыдущим "-1" : window: { documents: [-1, "current"] } .
      • таким образом, мы создаем в этом окне массив значений: $push: "$value"
      • и обратите внимание, что мы позаботились о сортировке документов по дням : sortBy: { day: 1 } .
  • и затем:
    • фильтры в первом документе (который можно отметить по его массиву, имеющему только один элемент): { $eq: [ { $size: "$pair" }, 1 ] }
    • и отфильтровывает следующие документы, если они pair имеют одинаковые значения: { $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }