Zapytanie o limit / przesunięcie i licznik Mongoose

87

Trochę dziwne pytanie o wydajność zapytania ... Muszę uruchomić zapytanie, które oblicza całkowitą liczbę dokumentów i może również zwrócić zestaw wyników, który można ograniczyć i przesunąć.

Mam więc w sumie 57 dokumentów, a użytkownik chce, aby 10 dokumentów zostało przesuniętych o 20.

Przychodzą mi do głowy 2 sposoby na zrobienie tego, najpierw zapytanie o wszystkie 57 dokumentów (zwrócone jako tablica), a następnie użycie array.slice zwróci żądane dokumenty. Drugą opcją jest uruchomienie 2 zapytań, pierwszego przy użyciu natywnej metody „count” mongo, a następnie uruchomienie drugiego zapytania przy użyciu natywnych agregatorów $ limit i $ skip mongo.

Jak myślisz, które będzie lepiej skalowane? Robisz to wszystko w jednym zapytaniu lub uruchamiasz dwa oddzielne?

Edytować:

// 1 query
var limit = 10;
var offset = 20;

Animals.find({}, function (err, animals) {
    if (err) {
        return next(err);
    }

    res.send({count: animals.length, animals: animals.slice(offset, limit + offset)});
});


// 2 queries
Animals.find({}, {limit:10, skip:20} function (err, animals) {            
    if (err) {
        return next(err);
    }

    Animals.count({}, function (err, count) {
        if (err) {
            return next(err);
        }

        res.send({count: count, animals: animals});
    });
});
leepowell
źródło
Jestem jednak pewien, Mongoose domyślna count()funkcja w PHP nie bierze limitlub skippod uwagę, chyba że powiedziano tak właśnie działa jedno zapytanie limitu i pominąć, a następnie coraz licznik powinien dać najbardziej wydajnych rozwiązań tu prawdopodobnie. Jak jednak dowiesz się, że istnieje 57 dokumentów, jeśli nie wykonasz dwóch zapytań, aby policzyć to, co aktualnie tam jest? Czy masz stałą liczbę, która nigdy się nie zmienia? Jeśli nie, będziesz musiał zrobić zarówno pominięcie, jak i ograniczenie, a następnie zliczanie.
Sammaye
Przepraszam, mówiłem o użyciu rodzimej metody liczenia Mongodb.collection.find(<query>).count();
leepowell
Przepraszam, że to ja, źle odczytałem twoje pytanie. Hmmm właściwie nie jestem pewien, co byłoby lepsze, czy twój zestaw wyników zawsze będzie naprawdę niski, jak 57 dokumentów? Jeśli tak, to wycinek po stronie klienta może być o milisekundę bardziej wydajny.
Sammaye
Dodałem przykład do pierwotnego pytania, nie sądzę, aby dane kiedykolwiek osiągnęły poziom ponad 10000, ale potencjalnie może.
leepowell
Przy 10 tys. Rekordów można było zauważyć, że obsługa pamięci JS jest mniej wydajna niż count()funkcja MongoDB. count()Funkcja w MongoDB jest stosunkowo powolny, ale nadal jest dość dużo szybciej niż większość odmian po stronie klienta na większych zestawów i może być szybsze niż po stronie klienta liczenia tutaj ewentualnie. Ale ta część jest subiektywna dla twoich własnych testów. Pamiętaj, że wcześniej z łatwością policzyłem tablice o długości 10k, więc może to być szybsza strona klienta, bardzo trudno powiedzieć przy 10k elementach.
Sammaye

Odpowiedzi:

133

Proponuję użyć 2 zapytań:

  1. db.collection.count()zwróci całkowitą liczbę elementów. Ta wartość jest przechowywana gdzieś w Mongo i nie jest obliczana.

  2. db.collection.find().skip(20).limit(10)tutaj zakładam, że możesz użyć sortowania według jakiegoś pola, więc nie zapomnij dodać indeksu do tego pola. To zapytanie też będzie szybkie.

Myślę, że nie powinieneś odpytywać wszystkich pozycji, a następnie wykonać pomiń i weź, bo później, gdy będziesz mieć duże zbiory danych, będziesz miał problemy z przesyłaniem i przetwarzaniem danych.

user854301
źródło
1
To, co piszę, to tylko komentarz bez pretensji, ale słyszałem, że .skip()instrukcja jest ciężka dla procesora, ponieważ przechodzi na początek kolekcji i dociera do wartości określonej w parametrze .skip(). To może mieć realny wpływ na dużą kolekcję! Ale i tak nie wiem, który z nich jest najcięższy między użyciem, .skip()czy też zebrać całą kolekcję i przyciąć za pomocą JS ... Co o tym sądzisz?
Zachary Dahan
2
@Stuffix Słyszałem te same obawy dotyczące używania .skip(). Ta odpowiedź dotyka tego problemu i zaleca użycie filtru w polu daty. Można tego użyć z metodami .skip()& .take(). Wydaje się, że to dobry pomysł. Jednak mam problem z pytaniem tego OP, jak uzyskać liczbę wszystkich dokumentów. Jeśli filtr jest używany do zwalczania wpływu na wydajność .skip(), jak możemy uzyskać dokładną liczbę? Liczba przechowywana w bazie danych nie będzie odzwierciedlać naszego przefiltrowanego zestawu danych.
Michael Leanos
Cześć @MichaelLeanos, mam do czynienia z tym samym problemem: tj. Jak uzyskać zliczenie wszystkich dokumentów. Jeśli używany jest filtr, jak możemy uzyskać dokładną liczbę? Czy masz na to rozwiązanie?
virsha
@virsha, użyj, cursor.count()aby zwrócić liczbę przefiltrowanych zestawów dokumentów (nie wykona zapytania, zwróci liczbę dopasowanych dokumentów). Upewnij się, że właściwości filtru i kolejności są indeksowane i wszystko będzie dobrze.
user854301
@virsha Używanie cursor.count()powinno działać tak, jak wskazał @ user854301. Jednak ostatecznie dodałem punkt końcowy do mojego interfejsu API ( /api/my-colllection/stats), którego użyłem do zwrócenia różnych statystyk dotyczących moich kolekcji za pomocą funkcji db.collection.stats Mongoose . Ponieważ naprawdę potrzebowałem tego tylko dla mojego interfejsu, po prostu zapytałem punkt końcowy, aby zwrócić te informacje niezależnie od mojej paginacji po stronie serwera.
Michael Leanos
20

Zamiast używać dwóch oddzielnych zapytań, możesz użyć aggregate()w jednym zapytaniu:

Zagregowane „$ facet” można pobrać szybciej, całkowitą liczbę i dane z pominięciem i ograniczeniem

    db.collection.aggregate([

      //{$sort: {...}}

      //{$match:{...}}

      {$facet:{

        "stage1" : [ {"$group": {_id:null, count:{$sum:1}}} ],

        "stage2" : [ { "$skip": 0}, {"$limit": 2} ]
  
      }},
     
     {$unwind: "$stage1"},
  
      //output projection
     {$project:{
        count: "$stage1.count",
        data: "$stage2"
     }}

 ]);

wyjście w następujący sposób: -

[{
     count: 50,
     data: [
        {...},
        {...}
      ]
 }]

Zajrzyj również na https://docs.mongodb.com/manual/reference/operator/aggregation/facet/

Dhinesh Tak
źródło
2

Po samodzielnym rozwiązaniu tego problemu chciałbym skorzystać z odpowiedzi użytkownika854301.

Mongoose ^ 4.13.8 Udało mi się użyć funkcji o nazwie, toConstructor()która pozwoliła mi uniknąć wielokrotnego budowania zapytania po zastosowaniu filtrów. Wiem, że ta funkcja jest również dostępna w starszych wersjach, ale musisz sprawdzić dokumentację Mongoose, aby to potwierdzić.

Poniższe wykorzystuje obietnice Bluebird:

let schema = Query.find({ name: 'bloggs', age: { $gt: 30 } });

// save the query as a 'template'
let query = schema.toConstructor();

return Promise.join(
    schema.count().exec(),
    query().limit(limit).skip(skip).exec(),

    function (total, data) {
        return { data: data, total: total }
    }
);

Teraz zapytanie zliczające zwróci całkowitą liczbę dopasowanych rekordów, a zwrócone dane będą podzbiorem wszystkich rekordów.

Zwróć uwagę na () wokół query (), które konstruuje zapytanie.

oli_taz
źródło
1

Jest biblioteka, która zrobi to wszystko za Ciebie, sprawdź mongoose-paginate-v2

Dev01
źródło
0
db.collection_name.aggregate([
    { '$match'    : { } },
    { '$sort'     : { '_id' : -1 } },
    { '$facet'    : {
        metadata: [ { $count: "total" } ],
        data: [ { $skip: 1 }, { $limit: 10 },{ '$project' : {"_id":0} } ] // add projection here wish you re-shape the docs
    } }
] )

Zamiast używać dwóch zapytań, aby znaleźć całkowitą liczbę i pominąć dopasowany rekord.
$ facet to najlepszy i zoptymalizowany sposób.

  1. Dopasuj rekord
  2. Znajdź total_count
  3. pomiń zapis
  4. A także może zmienić kształt danych zgodnie z naszymi potrzebami w zapytaniu.
SANJEEV RAVI
źródło
1
Dodaj wyjaśnienie do swojej odpowiedzi, aby inni mogli się z niej czegoś nauczyć
Nico Haase,