التخطي والتحديد: تقسيم الصفحات بالإزاحة
سينفذ المتعلمون تقسيم الصفحات التقليدي المعتمد على أرقام الصفحات باستخدام skip() و limit()، ويقيسون كلفته من حيث الأداء على المجموعات الكبيرة.
التخطي والتحديد: تقسيم الصفحات بالإزاحة درس مجاني في MongoDB Academy على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في MongoDB Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة MongoDB Academy 4 دروس في المجموع.
بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.
What Is Offset Pagination?
Offset pagination—also called page-number pagination—divides results into fixed-size pages and uses a page number to determine how far into the result set to start. Page 1 shows items 1-20, page 2 shows items 21-40, and so on. In MongoDB this is implemented with skip() to jump over earlier results and limit() to restrict how many documents are returned per page.
Using skip() and limit()
limit(N) tells the cursor to return at most N documents. skip(N) tells MongoDB to skip the first N documents before returning any results. Together they implement page-number pagination: to get page P with size S items per page, use skip((P-1)*S) and limit(S).
const PAGE = 3;
const PAGE_SIZE = 20;
// Page 3 of 20 results per page
const products = await db.collection('products')
.find({ isActive: true })
.sort({ createdAt: -1 })
.skip((PAGE - 1) * PAGE_SIZE) // skip 40 docs (pages 1 and 2)
.limit(PAGE_SIZE) // return next 20
.toArray();
console.log('Page 3 results:', products.length);Counting Total Pages
Offset pagination usually requires a total document count to display page numbers in the UI. Use countDocuments(filter) to count matching documents before applying pagination. Run the count and the paged query in parallel to avoid adding latency. The total count divided by the page size (rounded up) gives the total number of pages.
const filter = { isActive: true };
const [total, results] = await Promise.all([
db.collection('products').countDocuments(filter),
db.collection('products')
.find(filter)
.sort({ createdAt: -1 })
.skip((PAGE - 1) * PAGE_SIZE)
.limit(PAGE_SIZE)
.toArray()
]);
const totalPages = Math.ceil(total / PAGE_SIZE);
console.log('Total:', total, 'Pages:', totalPages);The Hidden Cost of skip()
MongoDB implements skip() by scanning and discarding the first N documents. Even with an index, MongoDB must walk through and count N index entries before returning results. On page 1, skip is 0—fast. On page 500 with 20 items per page, skip is 9980—MongoDB must traverse nearly 10,000 entries just to find where page 500 starts. This is the core performance problem with offset pagination.
O(skip + limit) Query Complexity
The time to execute a skip/limit query grows linearly with the skip amount. The query cost is O(skip + limit)—the server must examine skip documents before returning limit documents. For page 1 this is O(20); for page 1000 with 20 items per page it is O(20020). As users navigate to higher page numbers, queries get progressively slower, often going from milliseconds to seconds on large collections.
When Offset Pagination Is Acceptable
Despite its performance limitations, offset pagination is acceptable in these scenarios: (1) the collection has fewer than a few thousand documents; (2) users rarely navigate beyond the first few pages; (3) the feature requires jumping directly to a page number (e.g., 'go to page 47'). Many admin dashboards and search results with low page depth fit this profile. Use keyset pagination for infinite scroll or large data sets.
Implementing an API Endpoint With Offset Pagination
A typical REST list endpoint accepts page and limit query parameters, validates them, and applies skip/limit accordingly. Always cap the maximum limit to prevent clients from requesting thousands of documents in a single call, which would exhaust server memory.
// Express route: GET /api/products?page=2&limit=20
async function listProducts(req, res) {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20); // cap at 100
const skip = (page - 1) * limit;
const [total, items] = await Promise.all([
Product.countDocuments({ isActive: true }),
Product.find({ isActive: true }).sort('-createdAt').skip(skip).limit(limit).lean()
]);
res.json({ page, limit, total, totalPages: Math.ceil(total / limit), items });
}Data Consistency Issues With Offset Pagination
Offset pagination has a subtle correctness problem: if documents are inserted or deleted between page requests, items can shift positions in the sorted result set. A document inserted between page 1 and page 2 pushes every subsequent document forward, causing one item to appear on both pages (duplicate) or be skipped entirely. This is called the page drift problem and is inherent to offset pagination.
Estimating vs Exact Count
For very large collections, countDocuments(filter) can be slow because it scans the index. An alternative is estimatedDocumentCount(), which is O(1) but counts all documents in the collection without applying a filter. For simple cases where you want the total without filtering, the estimated count is much faster. For filtered counts on large collections, consider caching the count or using Atlas's faceted search.
// O(1) but no filter support
const approxTotal = await db.collection('products').estimatedDocumentCount();
// Exact count with filter (slower on large collections)
const exactTotal = await db.collection('products').countDocuments({ isActive: true });Combining skip/limit With Projections
Always combine pagination with a tight projection for list endpoints. Fetching all fields while paginating defeats the purpose: you're still paying to transfer full document payloads for every page. A projection that returns only summary fields (name, price, thumbnail) reduces bandwidth by 80-95% compared to fetching full documents, making pagination viable at larger page offsets.
const SUMMARY = { _id: 1, name: 1, price: 1, thumbnailUrl: 1, rating: 1 };
const items = await db.collection('products')
.find({ isActive: true })
.projection(SUMMARY)
.sort({ rating: -1 })
.skip((PAGE - 1) * PAGE_SIZE)
.limit(PAGE_SIZE)
.toArray();When to Switch to Keyset Pagination
Switch from offset to keyset (cursor) pagination when: users scroll infinitely through results (no page numbers needed), the collection has more than 100,000 documents, page load times increase noticeably for higher page numbers, or data changes frequently between page requests. Keyset pagination is always O(log n) regardless of position in the result set, because it uses a range query on an indexed field instead of skip.
Quick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: offset pagination uses skip((page-1)*size) and limit(size) to fetch a page, skip() scans and discards documents so deep pages become progressively slower, and offset pagination is acceptable for small collections or shallow page depths but keyset pagination is better at scale. Next up we implement keyset pagination with range queries for consistent O(log n) performance.
الأسئلة الشائعة
هل درس «التخطي والتحديد: تقسيم الصفحات بالإزاحة» مجاني؟
نعم — نص درس «التخطي والتحديد: تقسيم الصفحات بالإزاحة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة MongoDB Academy، انتقل إلى CoddyKit PRO. تتضمن دورة MongoDB Academy 4 دروس في المجموع.
ماذا ستتعلم في «التخطي والتحديد: تقسيم الصفحات بالإزاحة»؟
سينفذ المتعلمون تقسيم الصفحات التقليدي المعتمد على أرقام الصفحات باستخدام skip() و limit()، ويقيسون كلفته من حيث الأداء على المجموعات الكبيرة. تتمرن على MongoDB Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ MongoDB Academy؟
لا تُشترط خبرة سابقة. MongoDB Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «التخطي والتحديد: تقسيم الصفحات بالإزاحة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس MongoDB Academy هذا؟
نعم. كل درس في MongoDB Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- الفرز باستخدام sort() ومفاتيح متعددة
- التخطي والتحديد: تقسيم الصفحات بالإزاحة
- تقسيم الصفحات بمفتاح المجموعة باستخدام استعلامات النطاق
- دمج الفرز والتخطي والتحديد والإسقاطات