Ограничение / смещение и счетчик 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)здесь я предполагаю, что вы можете использовать сортировку по какому-либо полю, поэтому не забудьте добавить индекс в это поле. Этот запрос тоже будет быстрым.

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

user854301
источник
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 (), который создает запрос.

oli_taz
источник
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
Пожалуйста, добавьте пояснения к своему ответу, чтобы другие могли извлечь из него уроки
Нико Хаасе