Sort and Paginate

Sorting, Limiting, and Paginating Results

$sort takes the same document as cursor.sort(); $limit and $skip take a positive integer. Their power is in where you put them, because the optimizer treats the trio as one unit: put $sort where an index can serve it and the sort disappears. Explaining [{ $match: { status: 'delivered' } }, { $sort: { placedAt: -1 } }, { $limit: 3 }] against status_1_placedAt_-1 gives LIMIT over FETCH over IXSCAN, with nReturned: 3, totalKeysExamined: 3, totalDocsExamined: 3 and no SORT node at all. Sort on an unindexed field and you get { stage: 'SORT', sortPattern: { shipping: -1 }, memLimit: 104857600, limitAmount: 2, inputStage: { stage: 'COLLSCAN' } }. That limitAmount: 2 proves the server pushed the $limit into the sort, keeping a two-element heap rather than ordering all 14 documents — a rewrite that happens only when $limit follows $sort directly.

Paging with skip, and why to stop

The textbook page-2 pipeline adds { $skip: 4 }, { $limit: 3 } after the sort. It does not scale: $skip discards documents the server already produced, so page 1,000 costs a thousand pages of work. An insert between requests also shifts every later page by a row, so a reader sees a duplicate or misses a record. Range pagination fixes both: remember the last row's sort key and ask for what follows.

Range pagination using the last row's sort key
db.orders.aggregate([
  { $match: { status: 'delivered',
              placedAt: { $lt: ISODate('2026-04-05T07:55:00Z') } } },
  { $sort: { placedAt: -1 } },
  { $limit: 3 },
  { $project: { _id: 1, placedAt: 1 } }
])
Output
[
  { _id: 'o-1006', placedAt: ISODate('2026-03-03T10:12:00.000Z') },
  { _id: 'o-1005', placedAt: ISODate('2026-02-27T19:30:00.000Z') },
  { _id: 'o-1003', placedAt: ISODate('2026-02-02T11:40:00.000Z') }
]

Make the cursor key unique or ties drop rows: sort by { placedAt: -1, _id: -1 } and compare the pair as { $or: [{ placedAt: { $lt: t } }, { placedAt: t, _id: { $lt: id } }] }. Hand clients one opaque token rather than two query parameters, so the sort key can change without breaking a bookmark. For the total such an endpoint also needs, $count collapses the stream to { n: <number> } — run count and page as two branches of one $facet (Faceted Results and Bucketing).