sql >> Database teknologi >  >> NoSQL >> MongoDB

Find antal af alle overlappende intervaller

Som du rigtigt nævner, er der forskellige tilgange med varierende kompleksitet, der er forbundet med deres udførelse. Dette dækker dybest set, hvordan de udføres, og hvilken du implementerer, afhænger faktisk af, hvilken data og din use case der er bedst egnet til.

Nuværende rækkeviddematch

MongoDB 3.6 $lookup

Den mest enkle tilgang kan anvendes ved at bruge den nye syntaks for $lookup operatør med MongoDB 3.6, der tillader en pipeline skal gives som udtryk for at "selv tilslutte sig" til samme samling. Dette kan grundlæggende forespørge samlingen igen for alle elementer, hvor starttidspunktet "eller" sluttid af det aktuelle dokument falder mellem de samme værdier af ethvert andet dokument, selvfølgelig ikke inklusive originalen:

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

Singlen $lookup udfører "join" på den samme samling, så du kan beholde de "aktuelle dokument"-værdier for "_id" , "starttid" og "sluttid" værdier via "let" mulighed for rørledningsfasen. Disse vil være tilgængelige som "lokale variabler" ved hjælp af $$ præfiks i efterfølgende "pipeline" af udtrykket.

Inden for denne "sub-pipeline" bruger du $match pipeline-stadiet og $expr forespørgselsoperator, som giver dig mulighed for at evaluere logiske udtryk for aggregeringsramme som en del af forespørgselsbetingelsen. Dette tillader sammenligning mellem værdier, da det vælger nye dokumenter, der matcher betingelserne.

Betingelserne ser blot efter de "behandlede dokumenter", hvor "_id" feltet er ikke lig med det "aktuelle dokument", $and hvor enten "starttid" $or "sluttid" værdier af det "aktuelle dokument" falder mellem de samme egenskaber for det "behandlede dokument". Bemærk her, at disse såvel som de respektive $gte og $lte operatører er "aggregationssammenligningsoperatører" og ikke "forespørgselsoperatoren" formular, som det returnerede resultat evalueret af $expr skal være boolesk i sammenhæng. Dette er, hvad aggregeringssammenligningsoperatørerne faktisk gør, og det er også den eneste måde at sende værdier til sammenligning.

Da vi kun ønsker "tællingen" af kampene, er $count pipeline fase bruges til at gøre dette. Resultatet af den samlede $lookup vil være et "single element"-array, hvor der var en optælling, eller et "tomt array", hvor der ikke var match til betingelserne.

En alternativ sag ville være at "udelade" $count fase og lad blot de matchende dokumenter returnere. Dette tillader nem identifikation, men som en "array indlejret i dokumentet" skal du være opmærksom på antallet af "overlapninger", der returneres som hele dokumenter, og at dette ikke forårsager et brud på BSON-grænsen på 16MB. I de fleste tilfælde burde dette være fint, men i tilfælde, hvor du forventer et stort antal overlapninger for et givent dokument, kan dette være en reel sag. Så det er virkelig noget mere at være opmærksom på.

$lookup pipeline-stadiet i denne sammenhæng vil "altid" returnere et array i resultat, selvom det er tomt. Navnet på outputegenskaben, der "flettes" ind i det eksisterende dokument, vil være "overlapninger" som angivet i "as" ejendom til $lookup scene.

Efter $lookup , kan vi så lave en simpel $match med et regulært forespørgselsudtryk, der anvender $exists test for 0 indeksværdi for output-array. Hvor der faktisk er noget indhold i arrayet og derfor "overlapper", vil betingelsen være sand, og dokumentet returneres, og viser enten antallet eller dokumenterne "overlappende" i henhold til dit valg.

Andre versioner - Forespørgsler om at "deltage"

Det alternative tilfælde, hvor din MongoDB mangler denne støtte, er at "tilslutte sig" manuelt ved at udstede de samme forespørgselsbetingelser som beskrevet ovenfor for hvert undersøgt 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);

Dette er i det væsentlige den samme logik, bortset fra at vi faktisk skal gå "tilbage til databasen" for at udstede forespørgslen til at matche de overlappende dokumenter. Denne gang er det "forespørgselsoperatorerne", der bruges til at finde, hvor de aktuelle dokumentværdier falder mellem værdierne i det behandlede dokument.

Fordi resultaterne allerede er returneret fra serveren, er der ingen BSON-begrænsning for at tilføje indhold til outputtet. Du har muligvis hukommelsesbegrænsninger, men det er et andet problem. Kort sagt returnerer vi arrayet i stedet for markøren via .toArray() så vi har de matchende dokumenter og kan simpelthen få adgang til arraylængden for at få en optælling. Hvis du faktisk ikke har brug for dokumenterne, så brug .count() i stedet for .find() er langt mere effektivt, da dokumentet ikke henter overhead.

Outputtet bliver så simpelthen slået sammen med det eksisterende dokument, hvor den anden vigtige skelnen er, at da afhandlinger er "flere forespørgsler", er der ingen måde at give betingelsen om, at de skal "matche" noget. Så dette efterlader os med at overveje, at der vil være resultater, hvor antallet (eller matrixlængden) er 0 og alt, hvad vi kan gøre på dette tidspunkt, er at returnere en null værdi, som vi senere kan .filter() fra resultatarrayet. Andre metoder til at iterere markøren anvender det samme grundlæggende princip om at "kassere" resultater, hvor vi ikke ønsker dem. Men intet forhindrer, at forespørgslen køres på serveren, og denne filtrering er "efterbehandling" i en eller anden form.

Reduktion af kompleksitet

Så ovenstående tilgange fungerer med strukturen som beskrevet, men den overordnede kompleksitet kræver naturligvis, at man for hvert dokument i det væsentlige skal undersøge hvert andet dokument i samlingen for at se efter overlapninger. Derfor, mens du bruger $lookup giver mulighed for en vis "effektivitet" i reduktion af transport- og responsomkostninger lider det stadig af det samme problem, at du stadig i det væsentlige sammenligner hvert dokument med alting.

En bedre løsning "hvor du kan få den til at passe" er i stedet at gemme en "hard value"*, der repræsenterer intervallet på hvert dokument. For eksempel kunne vi "antage", at der er solide "booking" perioder på en time inden for en dag for i alt 24 bookingperioder. Dette "kunne" være repræsenteret noget som:

{ "_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 organiseret på den måde, hvor der var en indstillet indikator for intervallet, reduceres kompleksiteten betydeligt, da det egentlig bare er et spørgsmål om at "gruppere" på intervalværdien fra arrayet i "booking" ejendom:

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

Og outputtet:

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

Det identificerer korrekt det for 10 og 11 intervaller både "A" og "D" indeholde overlapningen, mens "B" og "A" overlap på 12 . Andre intervaller og dokumenter, der matcher, er udelukket via de samme $exists test undtagen denne gang på 1 indeks (eller andet array-element er til stede) for at se, at der var "mere end ét" dokument i grupperingen, hvilket indikerer et overlap.

Dette bruger simpelthen $unwind aggregeringspipeline-stadiet for at "dekonstruere/denormalisere" matrixindholdet, så vi kan få adgang til de indre værdier til gruppering. Det er præcis, hvad der sker i $group trin, hvor den angivne "nøgle" er reservationsinterval-id'et og $push operator bruges til at "indsamle" data om det aktuelle dokument, som blev fundet i den gruppe. $match er som forklaret tidligere.

Dette kan endda udvides til alternativ præsentation:

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 output:

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

Det er en forenklet demonstration, men hvor de data, du har, ville tillade den den slags analyse, der kræves, er dette den langt mere effektive tilgang. Så hvis du kan beholde "granulariteten", der skal fastsættes til "indstillede" intervaller, som almindeligvis kan registreres på hvert dokument, så kan analysen og rapporteringen bruge sidstnævnte tilgang til hurtigt og effektivt at identificere sådanne overlapninger.

I bund og grund er det sådan, du alligevel ville implementere det, du grundlæggende nævnte som en "bedre" tilgang, og den første er en "lille" forbedring i forhold til det, du oprindeligt teoretiserede. Se hvilken der faktisk passer til din situation, men dette burde forklare implementeringen og forskellene.




  1. cappedMax virker ikke i winston-mongodb logger i Node.js på Ubuntu

  2. Krypter adgangskodefelter i mongodb

  3. auto-increment ved hjælp af loopback.js og MongoDB

  4. MongoDB - Fordel ved at bruge 12 bytes streng som unik identifikator i stedet for inkrementel værdi