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 $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.