Найти количество всех перекрывающихся интервалов - PullRequest
0 голосов
/ 24 апреля 2018

У меня есть startTime и endTime для всех подобных записей:

{
 startTime : 21345678
 endTime   : 31345678
}

Я пытаюсь найти количество всех конфликтов.Например, если есть две записи, и они перекрываются, количество конфликтов равно 1. Если есть три записи, и две из них перекрываются, конфликт равен 1. Если есть три записи и все три перекрываются, конфликт равен 3, то есть [(X1, X2), (X1, X3), (X2, X3)]

В качестве алгоритма я имею в виду сортировку данных по времени начала и для каждой отсортированной записи, проверку времени окончания и поиск записей со временем начала, меньшим, чем время окончания.Это будет время O (n 2 ).Лучшим подходом будет использование дерева интервалов и вставка каждой записи в дерево, а также определение количества при наложении.Это будет O (nlgn) время.

Я не очень много использовал mongoDB, так какой тип запроса я могу использовать для достижения чего-то подобного?

1 Ответ

0 голосов
/ 25 апреля 2018

Как вы правильно заметили, существуют разные подходы с различной сложностью, присущие их выполнению.Это в основном охватывает то, как они выполняются, и то, что вы реализуете, зависит от того, для каких ваших данных и вариантов использования лучше всего подходит.

Текущий диапазон соответствия

MongoDB 3.6 $ lookup

Самый простой подход может быть использован с использованием нового синтаксиса оператора $lookup с MongoDB 3.6, который позволяет указывать pipeline в качестве выражения для "самостоятельного соединения" с той же коллекцией.Это может в основном запросить коллекцию снова для любых элементов, в которых starttime "или" endtime текущего документа находятся между теми же значениями любого другого документа, не считая, конечно, оригинала:

db.getCollection('collection').aggregate([
  { "$lookup": {
    "from": "collection",
    "let": {
      "_id": "$_id",
      "starttime": "$starttime",
      "endtime": "$endtime"
    },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$and": [
            { "$ne": [ "$$_id", "$_id" },
            { "$or": [
              { "$and": [
                { "$gte": [ "$$starttime", "$starttime" ] },
                { "$lte": [ "$$starttime", "$endtime" ] }
              ]},
              { "$and": [
                { "$gte": [ "$$endtime", "$starttime" ] },
                { "$lte": [ "$$endtime", "$endtime" ] }
              ]}
            ]},
          ]
        },
        "as": "overlaps"
      }},
      { "$count": "count" },
    ]
  }},
  { "$match": { "overlaps.0": { "$exists": true }  } }
])

Одиночный $lookup выполняет «соединение» в той же коллекции, что позволяет вам сохранять значения «текущего документа» для значений "_id", "starttime" и "endtime" соответственно через "let" вариант этапа трубопровода.Они будут доступны как «локальные переменные» с использованием префикса $$ в последующем "pipeline" выражения.

В этом «под-конвейере» вы используете конвейер $matchstage и оператор запроса $expr, который позволяет оценивать логические выражения структуры агрегации как часть условия запроса.Это позволяет сравнивать значения при выборе новых документов, соответствующих условиям.

Условия просто ищут «обработанные документы», где поле "_id" не равно «текущему документу», $and, где либо значения "starttime" $or "endtime" «текущего документа» находятся между теми же свойствами «обработанного документа».Отметим здесь, что они, а также соответствующие операторы $gte и $lte являются "операторами сравнения агрегации" , а не "запросомоператор " form, так как возвращаемый результат, оцененный как $expr, должен быть boolean в контексте.Это то, что на самом деле делают операторы сравнения агрегации, и это также единственный способ передать значения для сравнения.

Так как нам нужно только «количество» совпадений, $count Для этого используется стадия конвейера.Результатом всего $lookup будет массив «один элемент», в котором было подсчет, или «пустой массив», в котором не было найдено соответствия условиям.

Anальтернативный вариант - «пропустить» этап $count и просто разрешить возврат соответствующих документов.Это позволяет легко идентифицировать, но как «массив, встроенный в документ», вам нужно помнить о количестве «перекрытий», которые будут возвращены как целые документы, и что это не приведет к нарушению предела BSON в 16 МБ.В большинстве случаев это должно быть хорошо, но для случаев, когда вы ожидаете большого количества совпадений для данного документа, это может быть реальным случаем.Так что на самом деле нужно знать больше.

Этап конвейера $lookup в этом контексте будет «всегда» возвращать массив в результате, даже если он пустой.Имя выходного свойства «слияние» с существующим документом будет "overlaps", как указано в свойстве "as" для этапа $lookup.

После $lookup, мы можем затем сделать простое $match с регулярным выражением запроса, используя тест $exists для значения индекса 0 выходного массива.Там, где на самом деле есть некоторый контент в массиве и, следовательно, «перекрывается», условие будет истинным, и документ вернется, показывая либо количество, либо документы, «перекрывающиеся» согласно вашему выбору.

Другие версии - Запросы к"join"

Альтернативный случай, когда вашей MongoDB не хватает этой поддержки, - это "присоединиться" вручную, выполнив те же условия запроса, которые описаны выше для каждого изученного документа:

db.getCollection('collection').find().map( d => {
  var overlaps = db.getCollection('collection').find({
    "_id": { "$ne": d._id },
    "$or": [
      { "starttime": { "$gte": d.starttime, "$lte": d.endtime } },
      { "endtime": { "$gte": d.starttime, "$lte": d.endtime } }
    ]
  }).toArray();

  return ( overlaps.length !== 0 ) 
    ? Object.assign(
        d,
        {
          "overlaps": {
            "count": overlaps.length,
            "documents": overlaps
          }
        }
      )
    : null;
}).filter(e => e != null);

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

Поскольку результаты уже возвращены с сервера, ограничения BSON на добавление контента в вывод отсутствуют. У вас могут быть ограничения памяти, но это другая проблема. Проще говоря, мы возвращаем массив, а не курсор через .toArray(), поэтому у нас есть соответствующие документы и мы можем просто получить доступ к длине массива, чтобы получить счет. Если вы на самом деле не нуждаетесь в документах, то использование .count() вместо .find() будет гораздо более эффективным, поскольку нет затрат на извлечение документа.

Выходные данные затем просто объединяются с существующим документом, где другое важное различие заключается в том, что, поскольку тезисы являются «множественными запросами», нет способа обеспечить условие, что они должны «соответствовать» чему-либо. Таким образом, это оставляет нас с учетом того, что будут результаты, в которых число (или длина массива) будет 0, и все, что мы можем сделать в это время, это вернуть значение null, которое мы можем позже .filter() из массива результатов. Другие методы итерации курсора используют тот же базовый принцип «отбрасывания» результатов, когда мы их не хотим. Но ничто не мешает выполнению запроса на сервере, и эта фильтрация является «постобработкой» в той или иной форме.

Снижение сложности

Таким образом, вышеприведенные подходы работают со структурой, как описано, но, конечно, общая сложность требует, чтобы для каждого документа вы по существу проверяли каждый другой документ в коллекции, чтобы найти совпадения. Поэтому, хотя использование $lookup допускает некоторую "эффективность" в сокращении затрат на транспортировку и ответ, оно по-прежнему испытывает ту же проблему, что вы все равно сравниваете каждый документ со всем.

Лучшее решение «где вы можете его подогнать» - вместо этого хранить «твердое значение» *, представляющее интервал в каждом документе. Например, мы можем «предположить», что в течение дня существуют надежные периоды «бронирования» продолжительностью один час, что в общей сложности составляет 24 периода бронирования. Это «можно» представить примерно так:

{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }

С данными, организованными так, где был установлен индикатор для интервала, сложность значительно снижается, поскольку на самом деле это просто вопрос "группировки" по значению интервала из массива в свойстве "booking":

db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } }
])

И вывод:

{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }

Это правильно определяет, что для интервалов 10 и 11 и "A", и "D" содержат перекрытия, тогда как "B" и "A" перекрываются на 12. Другие интервалы и сопоставление документов исключаются с помощью того же теста $exists, за исключением этого времени для индекса 1 (или наличия второго элемента массива), чтобы увидеть, что было «более одного» документа в группировке, следовательно, указывает на перекрытие.

Это просто использует стадию агрегации конвейера $unwind для «деконструкции / денормализации» содержимого массива, чтобы мы могли получить доступ к внутренним значениям для группировки. Это именно то, что происходит на этапе $group, где указанным «ключом» является идентификатор интервала бронирования, а оператор $push используется для «сбора» данных о текущем документ, который был найден в этой группе. $match такое же, как объяснено ранее.

Это может быть расширено для альтернативного представления:

db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } },
  { "$unwind": "$docs" },
  { "$group": {
    "_id": "$docs",
    "intervals": { "$push": "$_id" }  
  }}
])

С выводом:

{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }

Это упрощенная демонстрация, но там, где у вас есть данные, которые позволят использовать их для необходимого анализа, это гораздо более эффективный подход. Поэтому, если вы можете оставить «гранулярность» фиксированной, чтобы «установить» интервалы, которые обычно записываются в каждом документе, тогда анализ и отчетность могут использовать последний подход для быстрого и эффективного выявления таких перекрытий.

По сути, именно так вы бы в любом случае реализовали то, что вы в основном назвали «лучшим» подходом, и первым было «небольшое» улучшение по сравнению с тем, что вы изначально теоретизировали. Посмотрите, какой из них действительно соответствует вашей ситуации, но это должно объяснить реализацию и различия.

...