TGViewer
C# Geeks (.NET) C# Geeks (.NET) @csharpgeeks · 549 subscribers
Post #808 344
📌 ءPagination رو با Offset پیاده کنیم یا Cursor؟

یکی از اون تصمیم‌هاییه که موقع ساختن API خیلی راحت از کنارش رد می‌شیم.
مثلاً می‌خوایم لیست سفارش‌ها رو برگردونیم.
خب معلومه دیگه 😎
می‌زنیم:
SELECT *
FROM Orders
ORDER BY Id
OFFSET 100000
LIMIT 20;

یا توی EF Core:
var orders = await db.Orders
.OrderBy(x => x.Id)
.Skip(100000)
.Take(20)
.ToListAsync();

تموم شد رفت.
هم ساده‌ست، هم خواناست، هم برای صفحه‌بندی خیلی راحت می‌تونیم بگیم:
page=1
page=2
page=3
و الی آخر...
ولی یک لحظه صبر کن... 🤨
واقعاً فکر می‌کنی وقتی رسیدیم به صفحه‌ی مثلاً 10,000، دیتابیس میگه:
«چشم قربان، دقیقاً 20 تا رکوردت رو از این وسط برمی‌دارم»؟ 😎
نه دقیقاً! OFFSET به دیتابیس میگه:
«این 100 هزار رکورد اول رو رد کن، بعد 20 تای بعدی رو بده.»
یعنی رکوردهایی که قرار نیست به Application برسن، باز هم باید در سمت دیتابیس پردازش بشن.
خود PostgreSQL هم صراحتاً میگه رکوردهایی که توسط OFFSET رد می‌شن، همچنان باید توسط Server محاسبه بشن و OFFSETهای بزرگ می‌تونن inefficient باشن.
پس اگر دیتاست کوچیکه؟
احتمالاً اصلاً مسئله‌ی خاصی نداری.
ولی اگر داری با میلیون‌ها رکورد و Pagination عمیق سروکله می‌زنی، داستان فرق می‌کنه.
حالا بریم سراغ گزینه‌ی دوم:
🎯 Cursor / Keyset Pagination

اینجا به‌جای اینکه به دیتابیس بگیم:
«100 هزار تا رکورد رو رد کن»
می‌گیم:
«من تا اینجا اومدم؛ از بعدِ این رکورد ادامه بده.»
مثلاً:
SELECT *
FROM Orders
WHERE Id > 100000
ORDER BY Id
LIMIT 20;

یا در EF Core:
var orders = await db.Orders
.Where(x => x.Id > lastSeenId)
.OrderBy(x => x.Id)
.Take(20)
.ToListAsync();

اینجا دیگه مفهوم اصلی page number نیست.
مفهوم اصلی اینه:
🧠 من آخرین چیزی که دیدم چی بود؟
مثلاً Response اول:
{
"items": [...],
"nextCursor": "100020"
}

Request بعدی:
GET /orders?cursor=100020

و دیتابیس میگه:
WHERE Id > 100020
ORDER BY Id
LIMIT 20

اگر روی Id ایندکس داشته باشیم، دیتابیس می‌تونه خیلی مستقیم‌تر به محدوده‌ی موردنظر برسه.
ءMicrosoft هم در مستندات EF Core، برای Paginationهایی که فقط حرکت صفحه‌به‌صفحه لازم دارند، Keyset Pagination را به‌عنوان جایگزین مناسب Skip/Take معرفی می‌کند.
اما داستان فقط Performance نیست! 👀
فرض کن کاربر صفحه‌ی 2 رو گرفته.
بعد وسط کار، یک Order جدید وارد سیستم می‌شه.
اگر از OFFSET استفاده کنیم، موقع درخواست صفحه‌ی بعدی ممکنه مجموعه‌ی نتایج نسبت به درخواست قبلی جابه‌جا شده باشه و در شرایط تغییر همزمان داده‌ها، بعضی رکوردها دوباره دیده بشن یا بعضی‌ها از دست برن.
ولی Keyset میگه:
«من آخرین رکوردی که دیدم رو می‌دونم؛ از همون نقطه ادامه بده.»
به همین دلیل برای چیزهایی مثل:
📰 Feed
🛒 Order List
💬 Message List
📜 Activity Log
🔔 Notification List
و سیستم‌هایی که کاربر معمولاً فقط می‌خواد:
Next → Next → Next
بره جلو، Cursor Pagination خیلی جذاب می‌شه.
اماااااا... 😏
اینجا هم قرار نیست بگیم: Cursor > Offset
و تمام!
چون Cursor یک محدودیت مهم داره.
فرض کن کاربر می‌گه:
«برو صفحه 873!»

با Cursor این کار به اون سادگی Offset نیست.
چون Cursor اساساً برای حرکت ترتیبی روی یک Result Set طراحی شده، نه Jump کردن مستقیم به یک Page Number.
پس مثلاً برای یک: 👨‍💼 Admin Panel
که کاربر می‌خواد بگه:
Page 1 | 2 | 3 | ... | 50
و مستقیماً بره صفحه 30...
Offset Pagination
هنوز می‌تونه انتخاب کاملاً معقولی باشه.
اما برای یک: 📱 Infinite Scroll
که کاربر فقط میگه:
«بیشتر بیار» Cursor معمولاً انتخاب بهتریه.
پس اگر بخوام خیلی خلاصه تصمیم بگیرم:
🟢 Offset Pagination

وقتی:
▫️تعداد داده خیلی زیاد نیست
▫️کاربر باید مستقیماً به یک Page خاص بره
▫️ءUX بر اساس Page Number طراحی شده
▫️سادگی Implementation برات مهمه

🔵 Cursor / Keyset Pagination

وقتی:
▫️ءDataset بزرگه
▫️ءPagination عمیقه
▫️کاربر معمولاً Next/Previous می‌کنه
▫️ءInfinite Scroll داری
▫️داده‌ها مرتباً Insert/Delete می‌شن
▫️ءPerformance در صفحات عمیق مهمه

و یک نکته‌ی خیلی مهم:
اگر Pagination داری، Order باید deterministic و ترجیحاً unique باشه.
مثلاً فقط:
.OrderByDescending(x => x.CreatedAt)

ممکنه کافی نباشه، چون چند رکورد می‌تونن CreatedAt یکسان داشته باشن.
بهتره مثلاً:
.OrderByDescending(x => x.CreatedAt)
.ThenByDescending(x => x.Id)

داشته باشی تا ترتیب کاملاً مشخص باشه. Microsoft هم روی unique بودن ترتیب برای Pagination تأکید کرده.

🔖هشتگ‌ها:
#Pagination #OffsetPagination #CursorPagination #KeysetPagination
More from @csharpgeeks
  1. Sep 22, 2026یه مدتی قراره از دنیای NET. فاصله بگیرم، چون وقتشه برم سربازی. راستش نمیدونم این مدت رو چج…
  2. Sep 20, 2026🔥 حالا مشکل اصلی: Alert Storm فرض کن Database از دسترس خارج شده. ۱۰۰ Pod داری. هر Pod می‌…
  3. Sep 20, 2026🚨 طراحی سیستم Monitoring و Alerting در یک سیستم بزرگ فرض کن ساعت ۳ صبح است. سیستم شما با…
  4. Sep 19, 2026#Engineering_Leadership تصمیم نگرفتن هم یک تصمیم است یه چیز عجیب توی تیم‌های مهندسی: گاهی…
  5. Sep 19, 2026☑ چک‌لیست آماده‌سازی تیم، فرایندها و زیرساخت برای توسعه با AI توجه: هیچ چک‌لیستی جهان‌شمول…
  6. Sep 19, 2026📌پایان یک انتظار طولانی: اعتبارسنجی ناهمگام (Async Validation) در NET 11.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →