Zoals u terecht opmerkt, zijn er verschillende benaderingen met een variërende complexiteit die inherent zijn aan hun uitvoering. Dit dekt in principe hoe ze worden gedaan en welke u implementeert, hangt af van welke gegevens en gebruiksscenario het meest geschikt zijn voor.
Huidige bereikovereenkomst
MongoDB 3.6 $lookup
De meest eenvoudige benadering kan worden gebruikt met behulp van de nieuwe syntaxis van de $opzoeken
operator met MongoDB 3.6 die een pipeline
toestaat moet worden gegeven als de uitdrukking om "zichzelf toe te voegen" aan dezelfde verzameling. Dit kan in principe de collectie opnieuw opvragen voor alle items waar de starttime
"of" eindtijd
van het huidige document valt tussen dezelfde waarden van elk ander document, het origineel natuurlijk niet meegerekend:
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 } } }
])
De enkele $lookup
voert de "join" uit op dezelfde verzameling, zodat u de "huidige document"-waarden voor de "_id"
kunt behouden , "starttijd"
en "eindtijd"
waarden respectievelijk via de "let"
optie van de pijplijnfase. Deze zullen beschikbaar zijn als "lokale variabelen" met behulp van de $$
prefix in volgende "pijplijn"
van de uitdrukking.
Binnen deze "sub-pipeline" gebruik je de $match
pijplijnfase en de $expr
query-operator, waarmee u logische expressies van het aggregatieraamwerk kunt evalueren als onderdeel van de queryvoorwaarde. Dit maakt de vergelijking tussen waarden mogelijk terwijl het nieuwe documenten selecteert die aan de voorwaarden voldoen.
De voorwaarden zoeken gewoon naar de "verwerkte documenten" waar de "_id"
veld is niet gelijk aan het "huidige document", $and
waarbij ofwel de "starttijd"
$or
"eindtijd"
waarden van het "huidige document" vallen tussen dezelfde eigenschappen van het "verwerkte document". Hierbij opmerkend dat deze evenals de respectievelijke $gte
en $lte
operators zijn de "aggregatievergelijkingsoperators"
en niet de "query-operator"
formulier, als het geretourneerde resultaat geëvalueerd door $expr
moet booleaans
zijn in context. Dit is wat de aggregatievergelijkingsoperatoren feitelijk doen, en het is ook de enige manier om waarden voor vergelijking door te geven.
Omdat we alleen het "aantal" van de overeenkomsten willen, is de $count
Hiervoor wordt de pijplijnfase gebruikt. Het resultaat van de algemene $lookup
zal een array met "enkel element" zijn waar een telling was, of een "lege array" waar er geen overeenkomst was met de voorwaarden.
Een alternatief geval zou zijn om de $count
en laat de overeenkomende documenten gewoon terugkeren. Dit maakt een gemakkelijke identificatie mogelijk, maar als een "array ingebed in het document" moet u rekening houden met het aantal "overlappingen" dat als hele documenten wordt geretourneerd en dat dit geen overschrijding van de BSON-limiet van 16 MB veroorzaakt. In de meeste gevallen zou dit goed moeten zijn, maar voor gevallen waarin u een groot aantal overlappingen verwacht voor een bepaald document, kan dit een reëel geval zijn. Het is dus echt iets meer om op te letten.
De $lookup
pijplijnfase zal in deze context "altijd" een array in resultaat retourneren, zelfs als deze leeg is. De naam van de uitvoereigenschap "samenvoegen" in het bestaande document is "overlappingen"
zoals gespecificeerd in de "as"
eigendom toe aan de $lookup
podium.
De $lookup
volgen
, kunnen we dan een eenvoudige $match
doen
met een reguliere query-expressie die gebruikmaakt van de $exists
test voor de 0
indexwaarde van uitvoerarray. Waar er daadwerkelijk enige inhoud in de array is en daarom "overlapt", zal de voorwaarde waar zijn en wordt het document geretourneerd, waarbij ofwel de telling of de documenten "overlappen", volgens uw selectie.
Andere versies - Vragen om "aan te sluiten"
Het alternatieve geval waarin uw MongoDB deze ondersteuning niet heeft, is om handmatig "aan te sluiten" door dezelfde vraagvoorwaarden op te geven die hierboven zijn beschreven voor elk onderzocht document:
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);
Dit is in wezen dezelfde logica, behalve dat we eigenlijk "terug naar de database" moeten gaan om de query uit te voeren die overeenkomt met de overlappende documenten. Deze keer zijn het de "query-operators" die worden gebruikt om te bepalen waar de huidige documentwaarden tussen die van het verwerkte document vallen.
Omdat de resultaten al door de server worden geretourneerd, is er geen BSON-limietbeperking voor het toevoegen van inhoud aan de uitvoer. Mogelijk hebt u geheugenbeperkingen, maar dat is een ander probleem. Simpel gezegd, we retourneren de array in plaats van de cursor via .toArray()
dus we hebben de overeenkomende documenten en kunnen eenvoudig toegang krijgen tot de arraylengte om een telling te verkrijgen. Als je de documenten niet echt nodig hebt, gebruik dan .count()
in plaats van .find()
is veel efficiënter omdat er geen overheadkosten zijn voor het ophalen van het document.
De uitvoer wordt dan eenvoudigweg samengevoegd met het bestaande document, waarbij het andere belangrijke onderscheid is dat, aangezien scripties "meerdere zoekopdrachten" zijn, er geen manier is om de voorwaarde te stellen dat ze iets moeten "passen". Dus dit laat ons achter met de overweging dat er resultaten zullen zijn waarbij de telling ( of arraylengte ) 0
is en alles wat we op dit moment kunnen doen is een null
return retourneren waarde die we later kunnen .filter()
uit de resultatenreeks. Andere methoden voor het herhalen van de cursor maken gebruik van hetzelfde basisprincipe van het "weggooien" van resultaten waar we ze niet willen hebben. Maar niets verhindert dat de query op de server wordt uitgevoerd en deze filtering is in een of andere vorm "naverwerking".
Complexiteit verminderen
Dus de bovenstaande benaderingen werken met de structuur zoals beschreven, maar de algehele complexiteit vereist natuurlijk dat je voor elk document in wezen elk ander document in de verzameling moet onderzoeken om overlappingen te zoeken. Daarom tijdens het gebruik van $lookup
zorgt voor enige "efficiëntie" bij het verminderen van transport- en responsoverhead, heeft het nog steeds hetzelfde probleem dat u in wezen nog steeds elk document met alles vergelijkt.
Een betere oplossing "waar je het passend kunt maken" is om in plaats daarvan een "harde waarde"* op te slaan die representatief is voor het interval op elk document. We zouden bijvoorbeeld kunnen "veronderstellen" dat er solide "boekings" perioden zijn van één uur binnen een dag voor een totaal van 24 boekingsperioden. Dit "zou" kunnen worden weergegeven als:
{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }
Met zo georganiseerde gegevens waarbij er een vaste indicator voor het interval was, wordt de complexiteit aanzienlijk verminderd, omdat het eigenlijk gewoon een kwestie is van "groeperen" op de intervalwaarde uit de array binnen de "boeking"
eigendom:
db.booking.aggregate([
{ "$unwind": "$booking" },
{ "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
{ "$match": { "docs.1": { "$exists": true } } }
])
En de uitvoer:
{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }
Dat identificeert correct dat voor de 10
en 11
intervallen beide "A"
en "D"
de overlap bevatten, terwijl "B"
en "A"
overlap op 12
. Andere intervallen en overeenkomende documenten worden uitgesloten via dezelfde $exists
test behalve deze keer op de 1
index (of tweede array-element aanwezig) om te zien dat er "meer dan één" document in de groep was, wat een overlap aangeeft.
Dit gebruikt gewoon de $unwind
aggregatiepijplijnfase om de array-inhoud te "deconstrueren / denormaliseren", zodat we toegang hebben tot de innerlijke waarden voor groepering. Dit is precies wat er gebeurt in de $group
fase waarin de opgegeven "sleutel" de boekingsinterval-ID is en de $push
operator wordt gebruikt om gegevens te "verzamelen" over het huidige document dat in die groep is gevonden. De $match
is zoals eerder uitgelegd.
Dit kan zelfs worden uitgebreid voor een alternatieve presentatie:
db.booking.aggregate([
{ "$unwind": "$booking" },
{ "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
{ "$match": { "docs.1": { "$exists": true } } },
{ "$unwind": "$docs" },
{ "$group": {
"_id": "$docs",
"intervals": { "$push": "$_id" }
}}
])
Met uitgang:
{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }
Het is een vereenvoudigde demonstratie, maar waar de gegevens die je hebt het mogelijk maken voor het soort analyse dat nodig is, is dit de veel efficiëntere aanpak. Dus als u de "granulariteit" vast kunt houden aan "ingestelde" intervallen die algemeen op elk document kunnen worden vastgelegd, dan kunnen de analyse en rapportage de laatste benadering gebruiken om dergelijke overlappingen snel en efficiënt te identificeren.
In wezen is dit hoe je zou implementeren wat je eigenlijk noemde als een "betere" benadering, en de eerste is een "kleine" verbetering ten opzichte van wat je oorspronkelijk theoretiseerde. Kijk welke daadwerkelijk bij jouw situatie past, maar dit zou de implementatie en de verschillen moeten verklaren.