sql >> Databasteknik >  >> NoSQL >> MongoDB

Hitta antalet överlappande intervall

Som du helt riktigt nämner finns det olika tillvägagångssätt med varierande komplexitet som är inneboende i deras utförande. Detta täcker i princip hur de görs och vilken du implementerar beror på vilken data och användningsfall som är bäst lämpad för.

Nuvarande intervallmatchning

MongoDB 3.6 $lookup

Den enklaste metoden kan användas med den nya syntaxen för $lookup operatör med MongoDB 3.6 som tillåter en pipeline ges som uttrycket att "själv ansluta sig" till samma samling. Detta kan i princip fråga samlingen igen för alla objekt där starttid "eller" sluttid av det aktuella dokumentet faller mellan samma värden som något annat dokument, inte inklusive originalet, naturligtvis:

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

Singeln $lookup utför "join" på samma samling så att du kan behålla värdena för "current document" för "_id" , "starttid" och "sluttid" värden via "let" alternativet för pipelinesteget. Dessa kommer att vara tillgängliga som "lokala variabler" med hjälp av $$ prefix i efterföljande "pipeline" av uttrycket.

Inom denna "sub-pipeline" använder du $match pipeline-stadiet och $expr frågeoperator, som låter dig utvärdera logiska uttryck för aggregeringsramverket som en del av frågevillkoret. Detta möjliggör jämförelse mellan värden när det väljer nya dokument som matchar villkoren.

Villkoren letar helt enkelt efter de "behandlade dokumenten" där "_id" fältet är inte lika med det "aktuella dokumentet", $and där antingen "starttid" $or "sluttid" värdena för det "aktuella dokumentet" faller mellan samma egenskaper för det "behandlade dokumentet". Notera här att dessa samt respektive $gte och $lte operatorer är "aggregationsjämförelseoperatorer" och inte "query operator" form, som det returnerade resultatet utvärderat av $expr måste vara boolesk i sammanhanget. Detta är vad aggregationsjämförelseoperatorerna faktiskt gör, och det är också det enda sättet att skicka in värden för jämförelse.

Eftersom vi bara vill ha "antal" av matchningarna, är $count pipeline stage används för att göra detta. Resultatet av den övergripande $lookup kommer att vara en "single element"-array där det fanns en räkning, eller en "tom array" där det inte fanns någon matchning med villkoren.

Ett alternativt fall skulle vara att "utelämna" $count steg och låt helt enkelt de matchande dokumenten returneras. Detta möjliggör enkel identifiering, men som en "array inbäddad i dokumentet" måste du vara uppmärksam på antalet "överlappningar" som kommer att returneras som hela dokument och att detta inte orsakar ett brott mot BSON-gränsen på 16MB. I de flesta fall bör detta vara bra, men för fall där du förväntar dig ett stort antal överlappningar för ett visst dokument kan detta vara ett verkligt fall. Så det är verkligen något mer att vara medveten om.

$lookup pipeline-stadiet i detta sammanhang kommer "alltid" att returnera en array i resultat, även om den är tom. Namnet på utdataegenskapen som "slår samman" med det befintliga dokumentet kommer att vara "överlappar" som anges i "as" egendom till $lookup skede.

Följer $lookup , vi kan sedan göra en enkel $match med ett vanligt frågeuttryck som använder $exists testa för 0 indexvärde för utgångsmatrisen. Om det faktiskt finns något innehåll i arrayen och därför "överlappar" kommer villkoret att vara sant och dokumentet returneras, vilket visar antingen antalet eller dokumenten "överlappar" enligt ditt val.

Andra versioner – frågor att "gå med"

Det alternativa fallet där din MongoDB saknar detta stöd är att "gå med" manuellt genom att utfärda samma frågevillkor som beskrivs ovan för varje granskat dokument:

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);

Detta är i huvudsak samma logik förutom att vi faktiskt behöver gå "tillbaka till databasen" för att skicka frågan för att matcha de överlappande dokumenten. Den här gången är det "frågeoperatorerna" som används för att hitta var de aktuella dokumentvärdena ligger mellan de för det bearbetade dokumentet.

Eftersom resultaten redan returneras från servern finns det ingen BSON-begränsning för att lägga till innehåll till utdata. Du kanske har minnesbegränsningar, men det är en annan fråga. Enkelt uttryckt returnerar vi arrayen snarare än markören via .toArray() så vi har de matchande dokumenten och kan helt enkelt komma åt arraylängden för att få en räkning. Om du faktiskt inte behöver dokumenten, använd .count() istället för .find() är mycket effektivare eftersom det inte finns dokumentet som hämtar overhead.

Utdata slås sedan helt enkelt samman med det befintliga dokumentet, där den andra viktiga skillnaden är att eftersom avhandlingar är "flera frågor" finns det inget sätt att tillhandahålla villkoret att de måste "matcha" något. Så detta lämnar oss med tanke på att det kommer att finnas resultat där antalet (eller arraylängden) är 0 och allt vi kan göra just nu är att returnera en null värde som vi senare kan .filter() från resultatmatrisen. Andra metoder för att iterera markören använder samma grundläggande princip att "kassera" resultat där vi inte vill ha dem. Men ingenting hindrar frågan från att köras på servern och denna filtrering är "efterbearbetning" i någon eller annan form.

Minska komplexiteten

Så ovanstående tillvägagångssätt fungerar med strukturen enligt beskrivningen, men naturligtvis kräver den övergripande komplexiteten att du för varje dokument i princip måste granska alla andra dokument i samlingen för att leta efter överlappningar. Därför när du använder $lookup möjliggör viss "effektivitet" i minskningen av transport- och svarskostnader, lider det fortfarande av samma problem att du fortfarande i princip jämför varje dokument med allt.

En bättre lösning "där du kan få den att passa" är att istället lagra ett "hårt värde"* som representerar intervallet på varje dokument. Till exempel skulle vi kunna "förutsätta" att det finns solida "bokningsperioder" på en timme inom en dag för totalt 24 bokningsperioder. Detta "kunde" representeras något i stil med:

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

Med data organiserade så där det fanns en inställd indikator för intervallet reduceras komplexiteten avsevärt eftersom det egentligen bara är en fråga om att "gruppera" på intervallvärdet från arrayen inom "booking" egenskap:

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

Och utdata:

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

Det identifierar korrekt det för 10 och 11 intervall både "A" och "D" innehåller överlappningen, medan "B" och "A" överlappning på 12 . Andra intervall och dokumentmatchning exkluderas via samma $exists testa förutom den här gången på 1 index (eller det andra matriselementet är närvarande) för att se att det fanns "mer än ett" dokument i grupperingen, vilket indikerar en överlappning.

Detta använder helt enkelt $unwind aggregeringspipelinesteget för att "dekonstruera/denormalisera" arrayinnehållet så att vi kan komma åt de inre värdena för gruppering. Det här är exakt vad som händer i $group steg där "nyckeln" som tillhandahålls är bokningsintervallets ID och $push operator används för att "samla in" data om det aktuella dokumentet som hittades i den gruppen. $match är som förklarats tidigare.

Detta kan till och med utökas för alternativ presentation:

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

Med utgång:

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

Det är en förenklad demonstration, men där data du har skulle tillåta den typ av analys som krävs är detta det mycket effektivare tillvägagångssättet. Så om du kan behålla "granulariteten" som ska fixeras till "inställda" intervall som vanligtvis kan registreras på varje dokument, då kan analysen och rapporteringen använda det senare tillvägagångssättet för att snabbt och effektivt identifiera sådana överlappningar.

I grund och botten är det så här du skulle implementera det du i princip nämnde som ett "bättre" tillvägagångssätt ändå, och det första är en "lätt" förbättring jämfört med vad du ursprungligen teoretiserade. Se vilken som faktiskt passar din situation, men detta bör förklara implementeringen och skillnaderna.




  1. Mongodb returnerar flera sub-array-resultat och exkluderar andra returnerade resultat

  2. Lägg till ett fält i befintligt MongoDB-dokument (med Mongoose i Node.js)

  3. otillåten db-låstyp:-1

  4. Refererar till ett annat schema i Mongoose