MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Намерете броя на всички припокриващи се интервали

Както правилно споменахте, има различни подходи с различна сложност, присъщи на тяхното изпълнение. Това основно обхваща начина, по който се правят и кой ще внедрите всъщност зависи от това кои данни и случай на употреба са най-подходящи.

Съвпадение на текущия диапазон

MongoDB 3.6 $търсене

Най-простият подход може да се използва с помощта на новия синтаксис на $lookup оператор с MongoDB 3.6, който позволява тръбопровод да се даде като израз за „самостоятелно присъединяване“ към същата колекция. Това по същество може да направи запитване към колекцията отново за всички елементи, където starttime "или" краен час на текущия документ попада между същите стойности на всеки друг документ, без разбира се оригинала:

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 изпълнява "join" на същата колекция, което ви позволява да запазите стойностите на "текущ документ" за "_id" , "начален час" и "крайно време" стойности съответно чрез "let" вариант на етапа на тръбопровода. Те ще бъдат достъпни като "локални променливи" с помощта на $$ префикс в последващия "тръбопровод" на израза.

В рамките на този „подтръбопровод“ използвате $match етап на конвейер и $expr оператор на заявка, който ви позволява да оценявате логически изрази на рамката за агрегиране като част от условието на заявката. Това позволява сравнението между стойностите, докато избира нови документи, отговарящи на условията.

Условията просто търсят „обработените документи“, където „_id“ полето не е равно на „текущия документ“, $and където или "начален час" $or "краен час" стойностите на "текущия документ" попадат между същите свойства на "обработения документ". Тук отбелязваме, че тези, както и съответния $gte и $lte операторите са "оператори за сравнение на агрегиране" а не "оператор на заявка" формуляр, като върнатият резултат се оценява от $expr трябва да бъде булев в контекст. Това всъщност правят операторите за сравняване на агрегации и също така е единственият начин за предаване на стойности за сравнение.

Тъй като искаме само „броя“ на съвпаденията, $count етапът на тръбопровода се използва за това. Резултатът от цялостния $lookup ще бъде масив от „единичен елемент“, където имаше преброяване, или „празен масив“, където нямаше съответствие с условията.

Алтернативен случай би бил да "пропуснете" $count и просто оставете съответстващите документи да се върнат. Това позволява лесна идентификация, но като „масив, вграден в документа“, трябва да имате предвид броя на „припокриванията“, които ще бъдат върнати като цели документи, и че това не причинява нарушение на BSON ограничението от 16MB. В повечето случаи това трябва да е наред, но за случаите, в които очаквате голям брой припокривания за даден документ, това може да е реален случай. Така че това наистина е нещо повече, за което трябва да сте наясно.

$lookup етапът на тръбопровод в този контекст "винаги" ще върне масив в резултат, дори ако е празен. Името на изходното свойство „сливане“ в съществуващия документ ще бъде „припокриване“ както е посочено в "as" свойство към $lookup етап.

Следвайки $lookup , тогава можем да направим прост $match с регулярен израз за заявка, използващ $exists тест за 0 стойност на индекса на изходния масив. Когато действително има някакво съдържание в масива и следователно се „припокрива“, условието ще бъде вярно и документът ще бъде върнат, показвайки или броя, или документите, „припокриващи се“ според вашия избор.

Други версии - Заявки за „присъединяване“

Алтернативният случай, при който вашата 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 ] }

С данни, организирани по този начин, където има зададен индикатор за интервала, сложността е значително намалена, тъй като всъщност е просто въпрос на „групиране“ на стойността на интервала от масива в рамките на „rezervation“ свойство:

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 ] }

Това е опростена демонстрация, но когато данните, които имате, биха го позволили за необходимия вид анализ, това е много по-ефективният подход. Така че, ако можете да поддържате „подробността“ да бъде фиксирана на „зададени“ интервали, които обикновено могат да се записват във всеки документ, тогава анализът и докладването могат да използват последния подход за бързо и ефективно идентифициране на такива припокривания.

По същество това е начинът, по който бихте приложили това, което така или иначе споменахте като "по-добър" подход, а първият е "леко" подобрение спрямо това, което първоначално сте теоретизирали. Вижте кой всъщност отговаря на вашата ситуация, но това трябва да обясни изпълнението и разликите.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Как да върна резултатите от Mongoose от метода за намиране?

  2. Изчислени полета за групиране в MongoDB

  3. mongoDB добавя специфични данни към db Replica

  4. Конфигурирайте pymongo да използва низ _id вместо ObjectId

  5. как да преброя $lookup полета в mongo db?