Jak słusznie wspomniałeś, istnieją różne podejścia o różnej złożoności związanej z ich wykonaniem. Zasadniczo obejmuje to, w jaki sposób są one wykonywane i który z nich zostanie zaimplementowany, zależy od tego, do jakich danych i przypadku użycia najlepiej pasuje.
Aktualne dopasowanie zakresu
Wyszukiwanie MongoDB 3.6
Najprostsze podejście można zastosować przy użyciu nowej składni $oglądaj
operator z MongoDB 3.6, który pozwala na potok
należy podać jako wyrażenie „samoprzyłączanie się” do tej samej kolekcji. Może to zasadniczo ponownie wysłać zapytanie do kolekcji o dowolne elementy, w których starttime
"lub" czas zakończenia
bieżącego dokumentu mieści się pomiędzy tymi samymi wartościami każdego innego dokumentu, oczywiście nie licząc oryginału:
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 } } }
])
Pojedynczy $lookup
wykonuje "join" w tej samej kolekcji, co pozwala zachować wartości "bieżącego dokumentu" dla "_id"
, "czas rozpoczęcia"
i "czas zakończenia"
wartości odpowiednio przez "let"
opcja etapu rurociągu. Będą one dostępne jako „zmienne lokalne” za pomocą $$
prefiks w kolejnym "potoku"
wyrażenia.
W ramach tego „podpotoku” używasz $match
etap potoku i $expr
operator zapytania, który umożliwia ocenę wyrażeń logicznych struktury agregacji w ramach warunku zapytania. Pozwala to na porównanie między wartościami podczas wybierania nowych dokumentów spełniających warunki.
Warunki po prostu szukają "przetworzonych dokumentów", gdzie "_id"
pole nie jest równe "bieżącemu dokumentowi", $and
gdzie albo "czas rozpoczęcia"
$lub
"czas zakończenia"
wartości "bieżącego dokumentu" mieszczą się pomiędzy tymi samymi właściwościami "przetworzonego dokumentu". Należy zauważyć, że te, a także odpowiednie $gte
i $lte
operatorami są "operatory porównania agregacji"
a nie "operator zapytań"
formularz, jako zwrócony wynik oceniany przez $expr
musi być boolean
w kontekście. To właśnie robią operatory porównania agregacji i jest to również jedyny sposób przekazywania wartości do porównania.
Ponieważ chcemy tylko „liczby” dopasowań, $liczba
Służy do tego etap rurociągu. Wynik ogólnego $lookup
będzie tablicą „pojedynczego elementu”, w której była liczba, lub „pustą tablicą”, w której nie było zgodności z warunkami.
Alternatywnym przypadkiem byłoby „pominięcie” $count
etapie i po prostu pozwól, aby pasujące dokumenty wróciły. Pozwala to na łatwą identyfikację, ale jako „tablica osadzona w dokumencie” musisz pamiętać o liczbie „nakładek”, które zostaną zwrócone jako całe dokumenty i że nie spowoduje to naruszenia limitu BSON wynoszącego 16 MB. W większości przypadków powinno to być w porządku, ale w przypadkach, w których spodziewasz się dużej liczby nakładania się danego dokumentu, może to być prawdziwy przypadek. Więc naprawdę jest coś więcej, o czym należy pamiętać.
$lookup
etap potoku w tym kontekście „zawsze” zwróci w wyniku tablicę, nawet jeśli jest pusta. Nazwa właściwości wyjściowej "scalająca" z istniejącym dokumentem będzie miała postać "overlaps"
jak określono w "jako"
właściwość $lookup
scena.
Zgodnie z $lookup
, możemy wtedy wykonać proste $match
z regularnym wyrażeniem zapytania wykorzystującym $exists
test na 0
wartość indeksu tablicy wyjściowej. Tam, gdzie faktycznie w tablicy znajduje się jakaś zawartość, a zatem „nakłada się”, warunek będzie spełniony, a dokument zwrócony, pokazując liczbę lub dokumenty „nakładające się” zgodnie z wyborem.
Inne wersje – zapytania do „dołączenia”
Alternatywnym przypadkiem, w którym MongoDB nie ma tej obsługi, jest ręczne „dołączanie” przez wydawanie tych samych warunków zapytania opisanych powyżej dla każdego badanego dokumentu:
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);
Jest to zasadniczo ta sama logika, z tą różnicą, że musimy wrócić „z powrotem do bazy danych”, aby wysłać zapytanie pasujące do nakładających się dokumentów. Tym razem są to "operatory zapytań" używane do znalezienia, gdzie aktualne wartości dokumentu mieszczą się między tymi z przetworzonego dokumentu.
Ponieważ wyniki są już zwracane z serwera, nie ma limitu BSON na dodawanie zawartości do danych wyjściowych. Możesz mieć ograniczenia pamięci, ale to inny problem. Mówiąc najprościej, zwracamy tablicę, a nie kursor za pomocą .toArray()
więc mamy pasujące dokumenty i możemy po prostu uzyskać dostęp do długości tablicy, aby uzyskać liczbę. Jeśli faktycznie nie potrzebujesz dokumentów, użyj .count()
zamiast .find()
jest znacznie bardziej wydajny, ponieważ nie ma narzutu na pobieranie dokumentu.
Wynik jest następnie po prostu scalany z istniejącym dokumentem, gdzie inną ważną różnicą jest to, że skoro tezy są „wieloma zapytaniami”, nie ma możliwości podania warunku, że muszą coś „dopasować”. Pozostaje nam więc zastanowienie się, że będą wyniki, w których liczba (lub długość tablicy) wynosi 0
i wszystko, co możemy teraz zrobić, to zwrócić null
wartość, którą możemy później .filter()
z tablicy wyników. Inne metody iterowania kursora wykorzystują tę samą podstawową zasadę „odrzucania” wyników tam, gdzie ich nie chcemy. Ale nic nie zatrzymuje zapytania na serwerze, a to filtrowanie jest „przetwarzaniem końcowym” w takiej czy innej formie.
Zmniejszanie złożoności
Tak więc powyższe podejścia działają z opisaną strukturą, ale oczywiście ogólna złożoność wymaga, aby dla każdego dokumentu zasadniczo zbadać każdy inny dokument w kolekcji w celu znalezienia nakładania się. Dlatego podczas korzystania z $lookup
pozwala na pewną "wydajność" w zmniejszeniu kosztów transportu i odpowiedzi, nadal boryka się z tym samym problemem, że nadal zasadniczo porównujesz każdy dokument ze wszystkim.
Lepsze rozwiązanie, „gdzie możesz je dopasować” jest zamiast tego przechowywanie „twardej wartości”* reprezentującej interwał w każdym dokumencie. Na przykład możemy „założyć”, że istnieją stałe okresy „rezerwacji” trwające jedną godzinę w ciągu dnia, co daje łącznie 24 okresy rezerwacji. To „może” być reprezentowane w następujący sposób:
{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }
W przypadku danych zorganizowanych w ten sposób, gdzie istniał ustawiony wskaźnik dla interwału, złożoność jest znacznie zmniejszona, ponieważ tak naprawdę jest to tylko kwestia „grupowania” wartości interwału z tablicy w ramach „rezerwacji”
właściwość:
db.booking.aggregate([
{ "$unwind": "$booking" },
{ "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
{ "$match": { "docs.1": { "$exists": true } } }
])
A wynik:
{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }
To poprawnie identyfikuje to dla 10
i 11
interwały oba "A"
i "D"
zawierać nakładanie się, podczas gdy "B"
i "A"
nakładają się na 12
. Inne interwały i dopasowania dokumentów są wykluczane za pomocą tego samego $exists
test, z wyjątkiem tego czasu na 1
index ( lub obecność drugiego elementu tablicy ), aby zobaczyć, że w grupie było "więcej niż jeden" dokument, co wskazuje na nakładanie się.
To po prostu wykorzystuje $unwind
etap potoku agregacji w celu „dekonstruowania/denormalizacji” zawartości tablicy, dzięki czemu możemy uzyskać dostęp do wewnętrznych wartości w celu grupowania. Dokładnie to dzieje się w $group
etap, w którym podany „klucz” to identyfikator interwału rezerwacji i $wciśnij
Operator służy do "zbierania" danych o aktualnym dokumencie, który został znaleziony w tej grupie. $match
jest jak wyjaśniono wcześniej.
Można to nawet rozszerzyć o alternatywną prezentację:
db.booking.aggregate([
{ "$unwind": "$booking" },
{ "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
{ "$match": { "docs.1": { "$exists": true } } },
{ "$unwind": "$docs" },
{ "$group": {
"_id": "$docs",
"intervals": { "$push": "$_id" }
}}
])
Z wyjściem:
{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }
Jest to uproszczona demonstracja, ale tam, gdzie dane, które posiadasz, pozwalają na przeprowadzenie wymaganej analizy, to jest to znacznie bardziej wydajne podejście. Jeśli więc możesz utrzymać „ziarnistość” na stałe w „ustawionych” odstępach, które mogą być powszechnie rejestrowane w każdym dokumencie, wówczas analiza i raportowanie mogą wykorzystać to drugie podejście, aby szybko i skutecznie zidentyfikować takie nakładanie się.
Zasadniczo jest to sposób, w jaki i tak zaimplementowałbyś to, co w zasadzie wspomniałeś jako „lepsze” podejście, a pierwszym jest „niewielkie” ulepszenie w stosunku do tego, co pierwotnie teoretycznie. Zobacz, który z nich pasuje do Twojej sytuacji, ale to powinno wyjaśnić implementację i różnice.