MongoDB - phân trang


81

Khi sử dụng MongoDB, có bất kỳ mẫu đặc biệt nào để tạo chế độ xem được phân trang không? giả sử một blog liệt kê 10 bài đăng mới nhất nơi bạn có thể điều hướng trở lại các bài đăng cũ hơn.

Hay một người giải quyết nó bằng một chỉ mục trên ví dụ blogpost.publishdate và chỉ cần bỏ qua và giới hạn kết quả?


1
Tôi sẽ để cái này treo vì dường như có một số bất đồng về cách chính xác để tạo ra quy mô này.
Roger Johansson

Câu trả lời:


98

Sử dụng bỏ qua + giới hạn không phải là một cách tốt để phân trang khi hiệu suất là một vấn đề hoặc với các bộ sưu tập lớn; nó sẽ ngày càng chậm hơn khi bạn tăng số trang. Việc sử dụng tính năng bỏ qua yêu cầu máy chủ chuyển qua tất cả các tài liệu (hoặc giá trị chỉ mục) từ 0 đến giá trị bù đắp (bỏ qua).

Tốt hơn nhiều là sử dụng truy vấn phạm vi (+ giới hạn) trong đó bạn chuyển giá trị phạm vi của trang cuối cùng. Ví dụ: nếu bạn đang sắp xếp theo "ngày xuất bản", bạn sẽ đơn giản chuyển giá trị "ngày xuất bản" cuối cùng làm tiêu chí cho truy vấn để lấy trang dữ liệu tiếp theo.


4
Sẽ rất tuyệt khi xem một số tài liệu xác nhận rằng bỏ qua trong mongodb lặp qua tất cả các tài liệu.
Andrew Orsich

5
Ở đây bạn thực hiện: bỏ qua tài liệu Nếu có bất kỳ nơi nào khác cần cập nhật thông tin, vui lòng cho tôi biết.
Scott Hernandez

2
@ScottHernandez: Tôi có phân trang với các liên kết đến nhiều trang (như vậy: Trang: Đầu tiên, 2, 3, 4, 5, Cuối cùng) và sắp xếp trên tất cả các trường. Chỉ một trong các trường của tôi là duy nhất (và được lập chỉ mục), một truy vấn phạm vi có hoạt động cho trường hợp sử dụng này không? Tôi e là không, tôi chỉ muốn xác nhận xem nó có khả thi không. Cảm ơn.
user183037


7
Có vẻ như điều này sẽ không hoạt động nếu có nhiều tài liệu có cùng giá trị ngày xuất bản.
d512,

12
  1. Phân trang dựa trên phạm vi khó thực hiện nếu bạn cần sắp xếp các mục theo nhiều cách.
  2. Hãy nhớ rằng nếu giá trị trường của tham số sắp xếp không phải là duy nhất, thì phân trang dựa trên Phạm vi sẽ trở nên không đáng tin cậy.

Giải pháp khả thi: cố gắng đơn giản hóa mô tả, nghĩ xem liệu chúng ta có thể sắp xếp theo id hoặc một giá trị duy nhất nào đó không?

Và nếu chúng ta có thể, thì có thể sử dụng trang dựa trên phạm vi.

Cách phổ biến là sử dụng sort (), bỏ qua () và giới hạn () để thực hiện phân trang những gì được mô tả ở trên.


Bạn có thể tìm thấy một bài viết hay với các ví dụ về mã Python tại đây codementor.io/arpitbhayani/…
Gianfranco P. Ngày

1
Cảm ơn bạn - câu trả lời tuyệt vời! Tôi khó chịu khi mọi người đề xuất phân trang bằng cách sử dụng bộ lọc, ví dụ { _id: { $gt: ... } }... nó chỉ đơn giản là không hoạt động nếu sử dụng thứ tự tùy chỉnh - ví dụ .sort(...).
Nick Grealy

@NickGrealy Tôi đã làm theo hướng dẫn để thực hiện việc này và bây giờ tôi đang ở trong tình huống phân trang 'có vẻ như nó hoạt động nhưng tôi bị thiếu tài liệu vì tôi đang sử dụng ID mongo nhưng khi dữ liệu mới được chèn vào db, và sau đó bộ sưu tập được sắp xếp theo thứ tự bảng chữ cái nếu trang bắt đầu chứa các bản ghi bắt đầu A nhưng ID cao hơn các bản ghi bắt đầu AA vì chúng được chèn sau đó các bản ghi AA không được phân trang trả lại. Bỏ qua và giới hạn có phù hợp không? Tôi có trong khu vực 60 triệu tài liệu để tìm kiếm.
berimbolo

@berimbolo - điều này xứng đáng để trò chuyện - bạn sẽ không nhận được câu trả lời của mình ở đây trong các nhận xét. Câu hỏi: bạn mong đợi hành vi nào? Bạn đang làm việc với một hệ thống trực tiếp, với các bản ghi luôn được tạo và xóa. Nếu bạn yêu cầu lại ảnh chụp nhanh trực tiếp dữ liệu của mình cho mỗi lần tải trang mới, thì bạn sẽ mong đợi dữ liệu cơ bản của mình thay đổi. Hành vi nên là gì? Nếu bạn làm việc với ảnh chụp nhanh dữ liệu "thời điểm", bạn sẽ có "các trang cố định", nhưng cũng sẽ có dữ liệu "lỗi thời". Vấn đề bạn đang mô tả lớn đến mức nào và mọi người gặp phải nó thường xuyên như thế nào?
Nick Grealy

1
Nó chắc chắn đáng để nói chuyện, vấn đề của tôi là tôi đã truy xuất một tập tin theo thứ tự bảng chữ cái của biển số xe và cứ sau 15 phút áp dụng các bản cập nhật cho các biển số đã thay đổi (loại bỏ hoặc thêm vào), vấn đề là nếu một biển số mới được thêm vào và nó bắt đầu với một ví dụ A và do kích thước trang là cuối cùng trên trang, sau đó nếu được yêu cầu tiếp theo thì không có bản ghi nào được trả lại, tôi tin rằng (một giả định và một ví dụ giả định nhưng minh họa cho vấn đề của tôi) vì ID cao hơn bất kỳ cái nào khác trong bộ. Tôi đang xem xét việc sử dụng biển số đầy đủ để lái xe lớn hơn một phần của truy vấn bây giờ.
berimbolo

5

Đây là giải pháp tôi đã sử dụng khi bộ sưu tập của tôi quá lớn để trả về trong một truy vấn duy nhất. Nó tận dụng thứ tự vốn có của _idtrường và cho phép bạn lặp lại một tập hợp theo kích thước lô đã chỉ định.

Đây là mô-đun npm, mongoose-paging , mã đầy đủ bên dưới:

function promiseWhile(condition, action) {
  return new Promise(function(resolve, reject) {
    process.nextTick(function loop() {
      if(!condition()) {
        resolve();
      } else {
        action().then(loop).catch(reject);
      }
    });
  });
}

function findPaged(query, fields, options, iterator, cb) {
  var Model  = this,
    step     = options.step,
    cursor   = null,
    length   = null;

  promiseWhile(function() {
    return ( length===null || length > 0 );
  }, function() {
    return new Promise(function(resolve, reject) {

        if(cursor) query['_id'] = { $gt: cursor };

        Model.find(query, fields, options).sort({_id: 1}).limit(step).exec(function(err, items) {
          if(err) {
            reject(err);
          } else {
            length  = items.length;
            if(length > 0) {
              cursor  = items[length - 1]._id;
              iterator(items, function(err) {
                if(err) {
                  reject(err);
                } else {
                  resolve();
                }
              });
            } else {
              resolve();
            }
          }
        });
      });
  }).then(cb).catch(cb);

}

module.exports = function(schema) {
  schema.statics.findPaged = findPaged;
};

Đính kèm nó vào mô hình của bạn như thế này:

MySchema.plugin(findPaged);

Sau đó, truy vấn như thế này:

MyModel.findPaged(
  // mongoose query object, leave blank for all
  {source: 'email'},
  // fields to return, leave blank for all
  ['subject', 'message'],
  // number of results per page
  {step: 100},
  // iterator to call on each set of results
  function(results, cb) {
    console.log(results);
    // this is called repeatedly while until there are no more results.
    // results is an array of maximum length 100 containing the
    // results of your query

    // if all goes well
    cb();

    // if your async stuff has an error
    cb(err);
  },
  // function to call when finished looping
  function(err) {
    throw err;
    // this is called once there are no more results (err is null),
    // or if there is an error (then err is set)
  }
);

không biết tại sao câu trả lời này không có nhiều ủng hộ hơn. đây là một cách hiệu quả hơn để paginate hơn bỏ qua / hạn
nxmohamad

Tôi cũng đã đến bởi gói này, nhưng hiệu suất của nó như thế nào so với bỏ qua / giới hạn và câu trả lời được cung cấp bởi @Scott Hernandez?
Tanckom

4
Câu trả lời này sẽ hoạt động như thế nào, để sắp xếp trên bất kỳ trường nào khác?
Nick Grealy

1

Phân trang dựa trên phạm vi là có thể thực hiện được, nhưng bạn cần phải thông minh về cách bạn tối thiểu / tối đa truy vấn.

Nếu đủ khả năng, bạn nên thử lưu vào bộ nhớ đệm kết quả của một truy vấn trong một tập tin hoặc bộ sưu tập tạm thời. Nhờ bộ sưu tập TTL trong MongoDB, bạn có thể chèn kết quả của mình vào hai bộ sưu tập.

  1. Truy vấn Tìm kiếm + Người dùng + Tham số (TTL bất kỳ)
  2. Kết quả của truy vấn (TTL bất kỳ + khoảng thời gian làm sạch + 1)

Sử dụng cả hai đảm bảo bạn sẽ không nhận được kết quả từng phần khi TTL gần thời điểm hiện tại. Bạn có thể sử dụng một bộ đếm đơn giản khi bạn lưu trữ kết quả để thực hiện một truy vấn phạm vi RẤT đơn giản tại thời điểm đó.


1

Dưới đây là một ví dụ về việc truy xuất danh sách các Usertài liệu theo thứ tự CreatedDate( pageIndexdựa trên số không) bằng cách sử dụng trình điều khiển C # chính thức.

public void List<User> GetUsers() 
{
  var connectionString = "<a connection string>";
  var client = new MongoClient(connectionString);
  var server = client.GetServer();
  var database = server.GetDatabase("<a database name>");

  var sortBy = SortBy<User>.Descending(u => u.CreatedDate);
  var collection = database.GetCollection<User>("Users");
  var cursor = collection.FindAll();
  cursor.SetSortOrder(sortBy);

  cursor.Skip = pageIndex * pageSize;
  cursor.Limit = pageSize;
  return cursor.ToList();
}

Tất cả các hoạt động phân loại và phân trang được thực hiện ở phía máy chủ. Mặc dù đây là một ví dụ trong C #, tôi đoán điều tương tự có thể được áp dụng cho các cổng ngôn ngữ khác.

Xem http://docs.mongodb.org/ecosystem/tutorial/use-csharp-driver/#modification-a-cursor-before-enumerating-it .


0
    // file:ad-hoc.js
    // an example of using the less binary as pager in the bash shell
    //
    // call on the shell by:
    // mongo localhost:27017/mydb ad-hoc.js | less
    //
    // note ad-hoc.js must be in your current directory
    // replace the 27017 wit the port of your mongodb instance
    // replace the mydb with the name of the db you want to query
    //
    // create the connection obj
    conn = new Mongo();

    // set the db of the connection
    // replace the mydb with the name of the db you want to query
    db = conn.getDB("mydb");

    // replace the products with the name of the collection
    // populate my the products collection
    // this is just for demo purposes - you will probably have your data already
    for (var i=0;i<1000;i++ ) {
    db.products.insert(
        [
            { _id: i, item: "lamp", qty: 50, type: "desk" },
        ],
        { ordered: true }
    )
    }


    // replace the products with the name of the collection
    cursor = db.products.find();

    // print the collection contents
    while ( cursor.hasNext() ) {
        printjson( cursor.next() );
    }
    // eof file: ad-hoc.js
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.