Ограничение / смещение и счетчик Mongoose


87

Немного странно в отношении производительности запроса ... Мне нужно запустить запрос, который выполняет общее количество документов, а также может возвращать набор результатов, который может быть ограничен и смещен.

Итак, у меня 57 документов, и пользователь хочет, чтобы 10 документов были компенсированы на 20.

Я могу придумать два способа сделать это: сначала запросить все 57 документов (возвращенных в виде массива), а затем с помощью array.slice вернуть нужные документы. Второй вариант - запустить 2 запроса, первый - с использованием собственного метода «count» mongo, а затем запустить второй запрос с использованием собственных агрегаторов mongo $ limit и $ skip.

Как вы думаете, что лучше масштабируется? Выполнять все это одним запросом или запускать два отдельных запроса?

Редактировать:

// 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});
    });
});

Я не уверен насчет Mongoose, однако count()функция по умолчанию в PHP не принимает limitи не skipучитывает, если только не сказано, поэтому просто выполнение одного запроса лимита и пропуска, а затем получение счетчика должно дать, вероятно, наиболее эффективное решение. Однако как вы узнаете, что существует 57 документов, если вы не выполните два запроса, чтобы подсчитать, что там сейчас? У вас есть статический номер, который никогда не меняется? В противном случае вам нужно будет выполнить как пропуск, так и ограничение, а затем подсчет.
— Sammaye

Извините, я говорил об использовании собственного метода подсчета Mongodb.collection.find(<query>).count();
— leepowell

Извините, это был я, я неправильно понял ваш вопрос. Хммм на самом деле я не уверен, что было бы лучше, всегда ли ваш набор результатов будет действительно низким, например, 57 документов? Если это так, то срез на стороне клиента может быть на миллисекунду более производительным.
— Sammaye

Я добавил пример к исходному вопросу, я не думаю, что данные когда-либо достигнут 10 000+, но потенциально может.
— leepowell

При 10k записей вы могли видеть, что обработка памяти JS менее производительна, чем count()функция MongoDB. count()Функция в MongoDB является относительно медленной , но он по - прежнему в значительной степени так же быстро , как и большинства клиентских вариаций на больших наборах , и это может быть быстрее , чем на стороне клиента подсчет здесь возможно. Но эта часть субъективна для вашего собственного тестирования. Имейте в виду, что раньше я легко считал массивы длиной 10 тыс., Так что это может быть быстрее на стороне клиента, очень сложно сказать, что на 10 тыс. Элементов.
— Sammaye

Ответы:


133

Предлагаю вам использовать 2 запроса:

  1. db.collection.count()вернет общее количество элементов. Это значение хранится где-то в Mongo и не вычисляется.

  2. db.collection.find().skip(20).limit(10)здесь я предполагаю, что вы можете использовать сортировку по какому-либо полю, поэтому не забудьте добавить индекс в это поле. Этот запрос тоже будет быстрым.

Я думаю, что вам не следует запрашивать все элементы, а затем выполнять пропуск и прием, потому что позже, когда у вас будут большие данные, у вас возникнут проблемы с передачей и обработкой данных.


1
То, что я пишу, это просто комментарий без каких-либо претензий, но я слышал, что .skip()инструкция тяжелая для ЦП, потому что она идет в начало коллекции и достигает значения, указанного в параметре .skip(). Это может реально повлиять на большую коллекцию! Но я не знаю, какой из них самый тяжелый между использованием в .skip()любом случае или получением всей коллекции и обрезкой с помощью JS ... Как вы думаете?
— Захари Дахан,

2
@Stuffix Я слышал такие же опасения по поводу использования .skip(). Этот ответ затрагивает его и советует использовать фильтр в поле даты. Это можно использовать с помощью методов .skip()& .take(). Это кажется хорошей идеей. Однако у меня возникли проблемы с вопросом этого OP о том, как подсчитать общее количество документов. Если фильтр используется для борьбы с последствиями для производительности .skip(), как мы можем получить точный подсчет? Счетчик, хранящийся в базе данных, не будет отражать наш отфильтрованный набор данных.
— Майкл Леанос,

Привет, @MichaelLeanos, я столкнулся с той же проблемой: то есть как подсчитать общее количество документов. Если используется фильтр, как мы можем вести точный подсчет? Вы нашли для этого решение?
— virsha 01

@virsha, используйте cursor.count()для возврата количества отфильтрованных документов (он не будет выполнять запрос, он вернет вам количество сопоставленных документов). Убедитесь, что свойства фильтрации и упорядочения проиндексированы, и все будет в порядке.
— user854301 01

@virsha Использование cursor.count()должно работать, как указал @ user854301. Однако в итоге я добавил к моему API ( /api/my-colllection/stats) конечную точку, которую я использовал для возврата различной статистики по моим коллекциям с помощью функции Mongoose db.collection.stats . Поскольку мне это действительно было нужно только для моего интерфейса, я просто запросил конечную точку, чтобы вернуть эту информацию независимо от моей разбивки на страницы на стороне сервера.
— Майкл Леанос

20

Вместо использования двух отдельных запросов вы можете использовать aggregate()в одном запросе:

Совокупный "$ фасет" можно получить быстрее, общее количество и данные с пропуском и ограничением

    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"
     }}

 ]);

вывод следующим образом: -

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

Также посмотрите https://docs.mongodb.com/manual/reference/operator/aggregation/facet/


2

После того, как мне пришлось решить эту проблему самостоятельно, я хотел бы опираться на ответ user854301.

Mongoose ^ 4.13.8 Я смог использовать вызываемую функцию, toConstructor()которая позволила мне избежать многократного построения запроса при применении фильтров. Я знаю, что эта функция доступна и в более старых версиях, но вам нужно будет проверить документацию Mongoose, чтобы подтвердить это.

Далее используются обещания 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 }
    }
);

Теперь запрос на счетчик вернет общее количество сопоставленных записей, а возвращенные данные будут подмножеством общих записей.

Обратите внимание на () вокруг query (), который создает запрос.



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

Вместо использования двух запросов для нахождения общего количества и пропуска совпадающей записи.
$ facet - лучший и оптимизированный способ.

  1. Совпадение с рекордом
  2. Найдите total_count
  3. пропустить запись
  4. А также может изменять форму данных в соответствии с нашими потребностями в запросе.

1
Пожалуйста, добавьте пояснения к своему ответу, чтобы другие могли извлечь из него уроки
— Нико Хаасе
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.