ترقيم الصفحات بـ OFFSET يبدو بسيطاً حتى تصل إلى 500,000 صف لكل مستأجر. هنا تصبح أبطأ عملية في الـ API. كيف انتقلنا إلى Cursor Pagination في Go وماذا تعلمنا.
ترقيم الصفحات بـ OFFSET يبدو بسيطاً حتى تصل إلى 500,000 صف لكل مستأجر. هنا يتحول إلى أبطأ وأغلى عملية في الـ API. هذه قصة انتقالنا إلى Cursor Pagination في أنظمة Go المدعومة بـ PostgreSQL.
ما الذي ينكسر مع OFFSET عند التوسع
الاستعلام LIMIT 20 OFFSET 400 يبدو بريئاً. المشكلة أن PostgreSQL يجب أن يقرأ ويتجاهل أول 400 صف للوصول إلى الصفحة 21. لا يمكنه القفز مباشرة إلى الصف 401. في الصفحة 1000 مع 20 عنصراً لكل صفحة، يتجاهل المحرك 19,980 صفاً لإعادة 20 فقط.
في تطبيق SaaS حيث يتصفح المستأجرون ذوو البيانات الكبيرة سجلات المعاملات أو الطلبات أو سجلات التدقيق، يكون طلب الصفحة الخمسين أبطأ بشكل ملحوظ من الصفحة الأولى.
المشكلة الثانية هي انجراف النتائج: إذا أُضيف سجل جديد بين جلب الصفحة الأولى والثانية، يرى المستخدم سجلاً مكرراً. وإذا حُذف سجل، يفقد صفاً بصمت. في السجلات المالية وسجلات التدقيق، هذا غير مقبول.
كيف يعمل Cursor Pagination
ترقيم الصفحات بالمؤشر يستخدم علامة من آخر سجل مرئي كنقطة ارتكاز للصفحة التالية. بدلاً من تخطي N صفاً، تخبر قاعدة البيانات بإعادة الصفوف بعد قيمة محددة:
-- الصفحة الأولى: بدون مؤشر
SELECT id, created_at, amount
FROM transactions
WHERE tenant_id = $1
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- الصفحة التالية: مؤشر من آخر صف
SELECT id, created_at, amount
FROM transactions
WHERE tenant_id = $1
AND (created_at, id) < ($2, $3)
ORDER BY created_at DESC, id DESC
LIMIT 20;
الشرط (created_at, id) < ($2, $3) هو بحث نطاق واحد في B-tree. قاعدة البيانات تقفز مباشرة إلى نقطة الارتكاز دون مسح أي صفوف سابقة. تكلفة الصفحة الأولى والصفحة الخمسة آلاف متطابقة.
السبب في ضرورة عمود فريد للكسر المتساوي
إذا رتّبت فقط على created_at، يُشكّل صفان بنفس الطابع الزمني مشكلة: لا يمكن معرفة أيهما ينتمي لأي جانب من المؤشر. إضافة id كعمود ثانوي فريد يحل التعادلات بشكل حتمي.
للاستفادة الكاملة من هذا الاستعلام، أنشئ فهرساً مركّباً يتطابق مع الفلتر وترتيب الفرز:
CREATE INDEX idx_txns_tenant_cursor
ON transactions(tenant_id, status, created_at DESC, id DESC);
تنفيذ Go العملي
قيمة المؤشر يجب ترميزها للنقل عبر API. نضع القيم في كائن JSON ثم نُرمّزه بـ base64 لإخفاء تفاصيل المخطط وجعل تغيير تنسيق المؤشر لاحقاً أسهل:
type Cursor struct {
CreatedAt time.Time `json:"ca"`
ID int64 `json:"id"`
}
func EncodeCursor(t time.Time, id int64) string {
b, _ := json.Marshal(Cursor{CreatedAt: t, ID: id})
return base64.URLEncoding.EncodeToString(b)
}
في المعالج، نطلب limit+1 عنصراً لاكتشاف وجود صفحات إضافية دون استعلام count منفصل:
items, _ := repo.List(ctx, tenantID, cursor, limit+1)
hasMore := len(items) > limit
if hasMore {
items = items[:limit]
last := items[len(items)-1]
nextCursor = EncodeCursor(last.CreatedAt, last.ID)
}
اعتبارات عملية لمنتجات MENA
في منتجات SaaS المنتشرة في لبنان ومنطقة الشرق الأوسط، كثير من المستأجرين لديهم بيانات تاريخية ضخمة من فترات الترحيل. متجر تجزئة نقل سنتين من السجلات التاريخية سيعاني من بطء OFFSET فوراً لأي استعلام عميق في التاريخ. أسلوب المؤشر يتعامل مع هذا بشفافية تامة.
أسلوب المؤشر يجعل وظائف تصدير البيانات أبسط أيضاً. مهمة خلفية تمشي عبر بيانات مستأجر كاملة تستخدم نفس آلية المؤشر:
var cursor *Cursor
for {
page, _ := repo.List(ctx, tenantID, cursor, 500)
if len(page) == 0 { break }
processBatch(page)
last := page[len(page)-1]
cursor = &Cursor{CreatedAt: last.CreatedAt, ID: last.ID}
}
القيود التي يجب معرفتها
ترقيم الصفحات بالمؤشر لا يدعم الوصول العشوائي: لا يمكن القفز إلى الصفحة 47 مباشرة. إذا كان المنتج يحتاج واجهة "انتقل إلى الصفحة N"، فهذا الأسلوب لا يناسبه.
تغيير ترتيب الفرز ليس بسيطاً: مؤشر مُرمَّز لترتيب من الأحدث للأقدم لا يمكن استخدامه للعكس.
دروس من الإنتاج
ترقيم الصفحات بالمؤشر يستحق جهد التنفيذ لأي منتج SaaS سينمو إلى ملايين الصفوف لكل مستأجر:
- أضف دائماً عمود فريداً كعامل كسر التعادل في مفتاح الفرز.
- أنشئ الفهرس المركّب ليتطابق مع فلتر المستأجر ومفتاح الفرز الكامل.
- رمّز المؤشرات كرموز معتمة؛ لا تعرّض قيم قاعدة البيانات مباشرة في الـ API.
- استخدم
limit+1لاكتشاف الصفحات الإضافية دون استعلام count منفصل.
تحتاج مساعدة في تصميم طبقة البيانات؟
Voxire تصمم وتبني أنظمة API للـ SaaS في لبنان ومنطقة الشرق الأوسط. إذا كنت تحتاج مساعدة في تحسين أداء قوائم البيانات أو إعادة تصميم نقاط نهاية API لمستأجرين كبار، يسعدنا مساعدتك.
https://voxire.com/get-a-quote/



