0Pricing
MongoDB Academy · บทเรียน

การรวม sort, skip, limit และโพรเจกชัน

ผู้เรียนจะประกอบสายการค้นหาแบบเต็มรูปแบบด้วยตัวกรอง โพรเจกชัน การเรียงลำดับ และการแบ่งหน้า เพื่อขับเคลื่อนเอนด์พอยต์รายการที่สมจริง

การรวม sort, skip, limit และโพรเจกชัน เป็นบทเรียน MongoDB Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน MongoDB Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส MongoDB Academy มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

The Complete Query Chain

A production-quality MongoDB list query combines four components: a filter to select matching documents, a projection to select which fields to return, a sort to order the results, and pagination (skip/limit or keyset) to return one page at a time. Understanding how these compose and in what order MongoDB executes them is essential for writing correct and performant queries.

MongoDB's Internal Execution Order

Regardless of the order you chain methods in the driver, MongoDB always executes query components in this fixed sequence: 1) Filter → 2) Sort → 3) Skip → 4) Limit. Projection is applied as documents are read from the storage engine. This order matters: skip operates on the sorted result set, and limit caps the final output after skipping. You cannot change this execution order.

// These two are identical — order of chaining doesn't matter
db.products.find({}).sort({ price: -1 }).skip(20).limit(10);
db.products.find({}).limit(10).skip(20).sort({ price: -1 });
// MongoDB executes: filter -> sort -> skip -> limit

Building a List API Endpoint

Put it all together in a realistic list endpoint. The example below fetches page 2 of active products in the 'electronics' category, sorted by rating descending, returning only the fields needed for a product card UI. Each piece of the chain serves a specific purpose.

const filter = { category: 'electronics', isActive: true };
const projection = { _id: 1, name: 1, price: 1, thumbnailUrl: 1, rating: 1 };
const PAGE = 2;
const PAGE_SIZE = 20;

const products = await db.collection('products')
  .find(filter)                     // 1. filter
  .project(projection)              // projection
  .sort({ rating: -1, _id: -1 })   // 2. sort
  .skip((PAGE - 1) * PAGE_SIZE)    // 3. skip
  .limit(PAGE_SIZE)                 // 4. limit
  .toArray();

The Supporting Index

For the complete query chain to be efficient, you need an index that covers the filter and sort. The filter uses category and isActive (equality), and the sort uses rating and _id. The ideal compound index places equality fields first, then sort fields: { category: 1, isActive: 1, rating: -1, _id: -1 }.

// Index supporting the list API query
db.products.createIndex({ category: 1, isActive: 1, rating: -1, _id: -1 });

// Verify: explain should show IXSCAN and no SORT stage
db.products.find({ category: 'electronics', isActive: true })
  .sort({ rating: -1, _id: -1 })
  .skip(20)
  .limit(20)
  .explain('executionStats');

The Complete Mongoose Query Chain

Mongoose's fluent API makes the same query chain readable and type-safe. Chain .find(), .select(), .sort(), .skip(), and .limit(), then call .lean() for a plain POJO result (much faster than full Mongoose documents when you don't need lifecycle hooks or virtuals).

const products = await Product
  .find({ category: 'electronics', isActive: true })
  .select({ name: 1, price: 1, thumbnailUrl: 1, rating: 1, _id: 1 })
  .sort({ rating: -1, _id: -1 })
  .skip((PAGE - 1) * PAGE_SIZE)
  .limit(PAGE_SIZE)
  .lean();  // returns plain objects, not Mongoose Documents

console.log('Products on page:', products.length);

Returning Pagination Metadata

A well-designed list API response includes pagination metadata alongside the data array: the current page, page size, total document count, total page count, and whether there is a next/previous page. This lets clients render pagination controls without making a separate count request. Always return metadata and data together in one response object.

const [total, items] = await Promise.all([
  Product.countDocuments({ category: 'electronics', isActive: true }),
  Product.find({ category: 'electronics', isActive: true })
    .select({ name: 1, price: 1, thumbnailUrl: 1, rating: 1 })
    .sort({ rating: -1 })
    .skip((PAGE - 1) * PAGE_SIZE)
    .limit(PAGE_SIZE)
    .lean()
]);

res.json({
  data:       items,
  page:       PAGE,
  pageSize:   PAGE_SIZE,
  total:      total,
  totalPages: Math.ceil(total / PAGE_SIZE),
  hasNext:    PAGE * PAGE_SIZE < total,
  hasPrev:    PAGE > 1
});

Applying Default Values and Input Validation

Always validate and sanitise the page, limit, and sort query parameters before using them in a MongoDB query. Unchecked values can cause skip(-5) errors, enormous limit(999999) calls that exhaust memory, or sort injections if you pass user-provided sort keys directly. Whitelist allowed sort fields and enforce sane min/max bounds on page and limit.

const ALLOWED_SORT_FIELDS = new Set(['rating', 'price', 'createdAt']);

function parseListParams(query) {
  const page  = Math.max(1, parseInt(query.page)  || 1);
  const limit = Math.min(100, Math.max(1, parseInt(query.limit) || 20));
  const sortField = ALLOWED_SORT_FIELDS.has(query.sort) ? query.sort : 'createdAt';
  const sortDir   = query.order === 'asc' ? 1 : -1;
  return { page, limit, sort: { [sortField]: sortDir, _id: sortDir } };
}

Switching Between Offset and Keyset Pagination

You can offer both pagination modes from the same endpoint by checking for a cursor parameter (keyset) vs a page parameter (offset). When cursor is provided, apply the range filter and omit skip(). When only page is provided, use skip(). This lets you migrate clients gradually from offset to keyset pagination without breaking backward compatibility.

async function listItems(req, res) {
  const limit = 20;
  let query = Product.find({ isActive: true }).sort({ createdAt: -1, _id: -1 }).limit(limit);

  if (req.query.cursor) {
    const { createdAt, _id } = decodeCursor(req.query.cursor);
    query = query.where('$or').equals([
      { createdAt: { $lt: new Date(createdAt) } },
      { createdAt: new Date(createdAt), _id: { $lt: _id } }
    ]);
  } else if (req.query.page) {
    const page = Math.max(1, parseInt(req.query.page) || 1);
    query = query.skip((page - 1) * limit);
  }

  const items = await query.lean();
  res.json({ items, nextCursor: items.length === limit ? encodeCursor(items.at(-1)) : null });
}

Testing the Query Chain

Test your complete query chain with both unit tests (mock the driver) and integration tests (real MongoDB via an in-memory mongod or testcontainers). Seed the test collection with enough documents to verify that pagination boundaries are correct: that page 1 and page 2 together return exactly 2×pageSize distinct documents with no duplicates or gaps.

Caching Paginated Results

For read-heavy paginated APIs, consider caching responses at the HTTP level (Redis or a CDN) keyed by the full query string including page, sort, and filters. Cache TTLs of 10-60 seconds reduce database load dramatically for popular queries. Be aware that cached responses may return slightly stale data—acceptable for most use cases but not for financial or real-time dashboards.

Verifying the Full Chain With explain()

Run your complete find chain with explain('executionStats') to validate the full query plan. The ideal plan shows: IXSCAN for the filter, the same index scan serving the sort (no separate SORT stage), and nReturned equal to your limit value. Any unexpected COLLSCAN or SORT stage indicates a missing or ineffective index.

const stats = await db.collection('products')
  .find({ category: 'electronics', isActive: true })
  .sort({ rating: -1, _id: -1 })
  .skip(20)
  .limit(20)
  .explain('executionStats');

const stage = stats.executionStats.executionStages;
console.log('Stage:', stage.stage);        // should be LIMIT
console.log('Docs examined:', stats.executionStats.totalDocsExamined);
console.log('Docs returned:', stats.executionStats.nReturned);

Quick Check

Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.

Lesson Recap

In this lesson you learned: MongoDB always executes filter → sort → skip → limit regardless of method chaining order, the supporting index should place equality fields first then sort fields, and pagination metadata (total, hasNext, hasPrev) should be returned alongside the data array. Next up we explore importing and exporting data with mongoimport, mongoexport, and seed scripts.

คำถามที่พบบ่อย

บทเรียน “การรวม sort, skip, limit และโพรเจกชัน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การรวม sort, skip, limit และโพรเจกชัน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส MongoDB Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส MongoDB Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การรวม sort, skip, limit และโพรเจกชัน”

ผู้เรียนจะประกอบสายการค้นหาแบบเต็มรูปแบบด้วยตัวกรอง โพรเจกชัน การเรียงลำดับ และการแบ่งหน้า เพื่อขับเคลื่อนเอนด์พอยต์รายการที่สมจริง คุณปฏิบัติ MongoDB Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน MongoDB Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน MongoDB Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การรวม sort, skip, limit และโพรเจกชัน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน MongoDB Academy นี้ได้ไหม

ได้ บทเรียน MongoDB Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การเรียงลำดับด้วย sort() และหลายคีย์
  2. การข้ามและการจำกัด: การแบ่งหน้าแบบออฟเซ็ต
  3. การแบ่งหน้าแบบคีย์เซ็ตด้วยการค้นหาแบบช่วง
  4. การรวม sort, skip, limit และโพรเจกชัน
← กลับไปที่ MongoDB Academy