ترقيم الصفحات بالمؤشر أم بالإزاحة؟ أيهما الأنسب لـ API الخاص بك؟

مقارنة بين التصفح بالمرجع والتصفح بالإزاحة: انحراف الصفحات، تكلفة الإزاحة العميقة، SQL لمجموعة المفاتيح، أمثلة من Stripe و Slack، وكيفية اختبار كليهما في Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

31 أغسطس 2026

ترقيم الصفحات بالمؤشر أم بالإزاحة؟ أيهما الأنسب لـ API الخاص بك؟

Apidog للمؤسسات

النشر على الخوادم المحلية

SSO و RBAC

متوافق مع SOC 2

استكشف Apidog للمؤسسات

يواجه كل نقطة نهاية لقائمة نفس السؤال في النهاية: كيف تقسم مليوني طلب إلى صفحات يمكن للعميل تصفحها؟ اختر ترقيم الصفحات بالإزاحة (offset pagination) وستحصل على SQL بسيط بالإضافة إلى أرقام صفحات يفهمها المستخدمون. اختر ترقيم الصفحات القائم على المؤشر (cursor-based pagination) وستحصل على نتائج مستقرة بالإضافة إلى زمن استجابة ثابت على أي عمق، لكنك تتخلى عن "القفز إلى الصفحة 47".

تختار معظم الفرق الإزاحة لأنه الافتراضي في كل برنامج تعليمي. ثم يصل جدول الطلبات إلى عدة ملايين من الصفوف، وتبدأ الصفحة 4,000 في انتهاء المهلة، ويبلغ المستخدمون عن رؤية نفس السجل مرتين أثناء التمرير. يغطي هذا الدليل كيفية عمل كلا الأسلوبين، أين تتعطل الإزاحة، لماذا تستخدم Stripe و Slack المؤشرات، وكيفية اختبار أي من الأسلوبين بطلبات متسلسلة في Apidog. بحلول النهاية، ستعرف بالضبط أي منهما يناسب نقطة النهاية الخاصة بك.

إذا كنت تريد الصورة الأوسع أولاً، فإن دليل ترقيم الصفحات في API الخاص بنا يغطي كل استراتيجية جنبًا إلى جنب. هذه المقالة تتعمق في الاثنتين الأكثر أهمية.

كيف يعمل ترقيم الصفحات بالإزاحة

يرتبط ترقيم الصفحات بالإزاحة مباشرة بـ SQL. يرسل العميل رقم الصفحة وحجم الصفحة؛ ويقوم الخادم بترجمتها إلى LIMIT و OFFSET.

SELECT id, customer_id, total_cents, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 25 OFFSET 50;

يعيد هذا الاستعلام الصفحة 3 من قائمة طلباتك بواقع 25 صفًا لكل صفحة. يبدو الطلب كما يلي:

GET /v1/orders?page=3&per_page=25

واستجابة نموذجية:

{
  "data": [
    {
      "id": "ord_8821",
      "customer_id": "cus_1932",
      "total_cents": 4599,
      "created_at": "2026-08-30T14:22:07Z"
    }
  ],
  "page": 3,
  "per_page": 25,
  "total": 1848203,
  "total_pages": 73929
}

الجاذبية واضحة. يمكن للعملاء القفز إلى أي صفحة. يمكن للخادم إرجاع عدد إجمالي. يمكن لأي مطور بناءه في فترة ما بعد الظهر. لجدول إداري صغير، هذا هو الخيار الصحيح، ويقدم دليلنا خطوة بخطوة حول ترقيم الصفحات في REST APIs بناءً كاملاً باستخدام الإزاحة.

لكن الإزاحة تحمل مشكلتين هيكليتين، ولا تظهر أي منهما في مرحلة التطوير. تظهر كلتاهما في مرحلة الإنتاج.

المشكلة 1: انزياح الصفحة

تحسب الإزاحة الصفوف من أعلى النتيجة المرتبة. لا تعرف شيئًا عن الصفوف التي رآها العميل بالفعل. لذلك، عندما يتم إدراج أو حذف صفوف بين الطلبات، تتغير الصفحات تحت العميل.

لنفترض أن مستخدمًا قام بتحميل الصفحة 1 من الطلبات مرتبة من الأحدث إلى الأقدم، الصفوف من 1 إلى 25. بينما يقرأ، تصل 3 طلبات جديدة. يطلب الصفحة 2، وهي OFFSET 25. الصفوف 23 و 24 و 25 من الاستجابة الأولى تم دفعها الآن إلى المواضع 26 إلى 28. يراها المستخدم مرة أخرى. تكرارات.

الحذف يعكس الأمر. قم بإزالة 3 صفوف من الصفحة 1 بينما يقرأها المستخدم، و OFFSET 25 الآن يتخطى 3 صفوف لم يراها المستخدم أبدًا. فقدان صامت للبيانات، ولا يتلقى أحد خطأ.

بالنسبة لتقرير شهري لا يتصفحه أحد في الوقت الفعلي، يكون الانزياح غير ضار. بالنسبة لخلاصة الأنشطة، أو نقطة نهاية المزامنة، أو أي شيء يتصفحه نص برمجي صفحة تلو الأخرى بينما تستمر عمليات الكتابة، فإن الانزياح يعني سجلات مكررة أو مفقودة. يلاحظ المستهلكون ذلك.

المشكلة 2: الإزاحات العميقة تفحص كل ما تتخطاه

OFFSET 500000 لا ينتقل فوراً إلى الصف 500,001. تقوم قاعدة البيانات بمسح الفهرس عبر نصف مليون إدخال، وتتخلص منها، ثم تعيد 25 صفًا. تزداد التكلفة خطيًا مع العمق: O(n) حيث n هي الإزاحة.

الأرقام الملموسة تجعل هذا حقيقياً. على جدول طلبات Postgres يحتوي على مليوني صف وفهرس على created_at:

يوضح مقال ماركوس ويناند no-offset writeup على Use The Index, Luke هذه التكلفة بخطط الاستعلام ويستحق القراءة بالكامل. النمط في الإنتاج هو سجل استعلام بطيء تهيمن عليه طلبات الإزاحة العالية، غالبًا من زاحف واحد يتصفح كل صفحة من API العام الخاص بك بجد. عميل واحد، وتتضاعف قيمة p99 الخاصة بك.

كيف يعمل ترقيم الصفحات القائم على المؤشر

ترقيم الصفحات القائم على المؤشر، ويسمى أيضًا ترقيم الصفحات بـ keyset، يتجاهل عداد الصفوف. بدلاً من "تخطي 50 صفًا"، يقول العميل "أعطني الصفوف بعد هذا السجل المحدد". يحدد المؤشر آخر صف رآه العميل، بحيث يمكن للخادم الانتقال مباشرة إلى الدفعة التالية.

تستخدم SQL مقارنة بالصفوف على مفتاح الفرز بدلاً من OFFSET:

SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE (created_at, id) < ('2026-08-30T14:22:07Z', 'ord_8821')
ORDER BY created_at DESC, id DESC
LIMIT 25;

لاحظ المقارنة ذات العمودين. created_at وحدها ليست فريدة؛ يمكن أن يقع طلبان في نفس المللي ثانية، ومفتاح الفرز غير الفريد يعني تخطي الصفوف أو تكرارها عند حدود الصفحات. إضافة id كفاصل يجعل الترتيب كاملاً والترقيم دقيقاً. باستخدام فهرس مركب على (created_at, id)، تبحث قاعدة البيانات مباشرة عن الحد وتقرأ 25 إدخالاً. تكلفة الصفحة 1 والصفحة 60,000 متماثلة.

يجب ألا تعرض واجهة برمجة التطبيقات (API) هذه القيم الخام، مع ذلك. تقوم التطبيقات الحقيقية بتشفير مفتاح الفرز في رمز غير شفاف، عادةً Base64:

GET /v1/orders?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNDoyMjowN1oiLCJpZCI6Im9yZF84ODIxIn0

الغموض هو قرار تصميمي، وليس إخفاءً بحد ذاته. العملاء الذين لا يستطيعون تحليل المؤشر لا يمكنهم بناء عناوين URL يدويًا، مما يتيح لك حرية تغيير مفتاح الفرز، أو إضافة تلميح تجزيء، أو تبديل محركات التخزين دون تعطيل أي شخص. يصبح العقد هو "أعد ما أعطيناك"، لا أكثر.

المقايضة: لا توجد صفحة 47. المؤشر يعرف فقط "بعد هذا الصف"، لذلك يتحرك العملاء للأمام (وللخلف، إذا أصدرت مؤشرًا سابقًا) صفحة واحدة في كل مرة. لا تأتي الإحصائيات الإجمالية مجانًا أيضًا؛ العد هو استعلام منفصل. للتصاميم التي تكون فيها مجموعة البيانات نفسها ضخمة، يغطي دليلنا حول تصميم ترقيم صفحات API لملايين السجلات جانب التوسع بعمق أكبر.

المفاضلات في لمحة

البعد ترقيم الصفحات بالإزاحة ترقيم الصفحات القائم على المؤشر
القفز إلى صفحة عشوائية نعم، أي رقم صفحة لا، تصفح تسلسلي فقط
العدد الإجمالي / عدد الصفحات سهل التضمين استعلام عد منفصل
أداء الصفحات العميقة O(n)، يتدهور مع العمق O(1) لكل صفحة على أي عمق
الاستقرار تحت عمليات الكتابة ينزاح: تكرارات وفجوات مستقر، مثبت على صف
تكلفة البناء تافهة متوسطة: تشفير، فواصل، تصميم فهرس
متطلبات الترتيب أي ORDER BY يعمل يحتاج إلى مفتاح فرز فريد ومفهرس
تخزين عناوين URL للصفحات مؤقتًا سهل، عناوين URL متوقعة أصعب، المؤشرات تختلف حسب التصفح
تعقيد العميل منخفض منخفض، إذا كان الغلاف نظيفًا

تستحق إحدى النقاط الدقيقة في ذلك الجدول التأكيد: يتطلب ترقيم الصفحات بالمؤشر ترتيبًا حتميًا. إذا سمحت نقطة النهاية الخاصة بك للعملاء بالفرز حسب عمود قابل للتغيير وغير فريد مثل status، فإن منطق keyset يصبح مؤلمًا بسرعة. تتسامح الإزاحة مع الترتيب غير الدقيق؛ وتعاقبه المؤشرات.

أيهما يجب أن تختار؟

طابق الأسلوب مع كيفية استهلاك البيانات.

جداول ولوحات تحكم الإدارة: الإزاحة (offset). الأدوات الداخلية التي تحتوي على بضعة آلاف من الصفوف، البشر ينقرون على أرقام الصفحات، وعدد "1,848 نتيجة" مرئي. الانزياح لا يهم، ويبقى العمق سطحيًا، والقفز إلى الصفحة ميزة حقيقية. الإزاحة تفوز في تكلفة البناء.

خلاصات التمرير اللانهائي: المؤشر (cursor). لا أحد يقفز إلى الصفحة 47 من خلاصات الأخبار. المستخدمون يحملون دائمًا "المزيد"، وتحدث عمليات الكتابة باستمرار، والتكرارات مرئية ومحرجة. هذه هي الحالة النموذجية للمؤشر.

واجهات برمجة التطبيقات العامة (Public APIs): المؤشر. لا يمكنك التحكم في المستهلكين لديك. سيقوم أحدهم بكتابة حلقة تتصفح كل صفحة، ومع الإزاحة، تصبح الصفحات العميقة مشكلتك في الساعة 3 صباحًا. تحافظ المؤشرات على تكلفة كل صفحة منخفضة وتتيح لك تطوير الأمور الداخلية خلف الرمز غير الشفاف. يغطي دليل ترقيم الصفحات في REST API الخاص بنا اتفاقيات عناوين URL ورؤوس HTTP بالتفصيل.

عمليات التصدير والمزامنة: المؤشر. تتطلب مهمة دفعة تسحب جميع مليوني طلب ضمانين: عدم وجود صفوف مفقودة على الرغم من عمليات الكتابة المتزامنة، وتكلفة ثابتة لكل صفحة. لا توفر الإزاحة أيًا منهما. يوفر المؤشر أيضًا نقطة استئناف مجانية عندما تتوقف المهمة عند الصف 1.4 مليون.

القاعدة الأساسية الصادقة: الإزاحة للواجهات الصغيرة التي يتصفحها البشر بكثرة، وذات الأعداد الكبيرة؛ المؤشرات لأي شيء كبير، أو مباشر، أو عام.

كيف تتعامل واجهات برمجة التطبيقات الحقيقية مع الأمر

تستخدم **Stripe** ترقيم الصفحات القائم على المؤشر بالكامل. تقبل كل نقطة نهاية قائمة starting_after (معرف كائن) و limit، وتتضمن الاستجابات has_more. لجلب الصفحة التالية من المدفوعات، تمرر معرف آخر عملية دفع تلقيتها. توضح وثائق ترقيم الصفحات في Stripe النمط؛ لاحظ أنه لا يوجد عدد إجمالي في أي مكان، وهو إغفال متعمد نظرًا لحجم عمليات الكتابة لديهم.

لا يزال **GitHub's REST API** يعرض page و per_page في معظم نقاط النهاية، مع رؤوس Link تشير إلى الصفحات التالية والأخيرة. ولكن اقرأ وثائق ترقيم الصفحات في GitHub بعناية: فهي توجه العملاء لاتباع رأس Link حرفياً بدلاً من إنشاء عناوين URL للصفحات، وقد تحولت نقاط النهاية الأحدث إلى المؤشرات، وذلك تحديداً لأن التصفح العميق بالإزاحة عبر المستودعات الضخمة يسبب مشاكل.

قامت **Slack** بترحيل Web API الخاص بها إلى ترقيم الصفحات بالمؤشر وتصنفه الآن كنهج تستخدمه جميع الطرق الجديدة. الطرق مثل conversations.history تعيد response_metadata.next_cursor، ويعني المؤشر الفارغ أنك وصلت إلى النهاية، كما هو موضح في وثائق ترقيم الصفحات في Slack.

ثلاث واجهات برمجة تطبيقات عالية الحركة، واتجاه السفر في اتجاه واحد: نحو المؤشرات.

تصميم غلاف الاستجابة

تنجح واجهة برمجة تطبيقات المؤشر أو تفشل بناءً على غلافها. اجعلها مملة ويمكن التنبؤ بها:

{
  "data": [
    {
      "id": "ord_8846",
      "customer_id": "cus_2201",
      "total_cents": 12900,
      "created_at": "2026-08-30T16:01:44Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNjowMTo0NFoiLCJpZCI6Im9yZF84ODQ2In0"
}

أربع قواعد تجعلها قوية:

اختبار كلا الأسلوبين في Apidog

تختبئ أخطاء ترقيم الصفحات عند الحدود: الصفحة الأخيرة، الصفحة الفارغة، المؤشر الذي تم حذف صفه الأساسي. النقر اليدوي لن يكتشفها، لكن سيناريو الاختبار المتسلسل سيفعل ذلك، وهنا يكتسب Apidog مكانته في سير العمل.

لنقاط نهاية المؤشر، قم بإنشاء سيناريو اختبار بخطوتين:

  1. استدعِ نقطة النهاية واستخرج المؤشر. أضف معالجًا لاحقًا للطلب الأول باستخدام JSONPath $.next_cursor، وقم بتخزينه في متغير مثل nextCursor. يتيح لك Apidog نسخ JSONPath مباشرة من لوحة الاستجابة؛ التفسير الكامل موجود في كيفية تعيين التأكيدات واستخراج المتغيرات باستخدام JSONPath.
  2. كرر طلب الصفحة التالية. قم بتضمين طلب ثانٍ في خطوة ForEach أو حلقة، ومرر {{nextCursor}} كمعامل المؤشر، وأعد استخراج $.next_cursor في كل تكرار، واخرج عندما يكون has_more خطأ. تأكد في كل تمريرة من عدم تكرار أي id من الصفحة السابقة وأن حجم الصفحة لا يتجاوز limit.

بالنسبة لنقاط نهاية الإزاحة، ينطبق نفس الهيكل مع متغير عداد: قم بزيادة page، وتأكد من أن طول data يساوي per_page حتى الصفحة الأخيرة، وتأكد من بقاء total ثابتًا طوال التصفح.

ثم أضف حالات الحافة كخطوات خاصة بها، كل منها بتأكيدات صريحة:

بمجرد نجاح السيناريو محليًا، قم بتشغيله في CI عند كل دمج. حمّل Apidog مجانًا ويمكنك تشغيل سيناريو تصفح المؤشر الكامل، بما في ذلك الحلقات والتأكيدات، في أقل من نصف ساعة.

الأسئلة الشائعة

هل ترقيم الصفحات القائم على المؤشر أفضل دائمًا؟

لا. الإزاحة هي الأنسب عندما يحتاج المستخدمون إلى أرقام صفحات وإجماليات ووصول عشوائي عبر مجموعة بيانات متواضعة، وهو ما يصف معظم أدوات الإدارة الداخلية. المؤشرات أفضل عندما تكون مجموعة البيانات كبيرة، وعمليات الكتابة متكررة، أو كانت واجهة برمجة التطبيقات عامة. الوضع الفاشل هو الانتقال افتراضيًا إلى الإزاحة لنقطة نهاية قائمة عامة واكتشاف تكلفة O(n) بعد الإطلاق.

كيف أحصل على عدد إجمالي باستخدام ترقيم الصفحات القائم على المؤشر؟

قم بتشغيل استعلام SELECT COUNT(*) منفصل بنفس المرشحات، إما كنقطة نهاية مميزة أو كمعامل استعلام اختياري مثل include_count=true. قم بتخزينه مؤقتًا بشكل مكثف؛ يلبي عدد تقريبي يتم تحديثه كل دقيقة تقريبًا كل واجهة مستخدم. تتخطى Stripe الإجماليات تمامًا، مما يوضح لك مدى ندرة حاجة العملاء لها حقًا.

هل يمكنني تقديم كلا نمطي ترقيم الصفحات في نقطة نهاية واحدة؟

يمكنك ذلك، ويفعل GitHub ذلك بالفعل خلال فترة انتقاله، ولكن تجنب ذلك في واجهات برمجة التطبيقات الجديدة. يعني وجود نمطين مجموعتين من حالات الحافة، ومصفوفتي اختبار، وارتباك العميل حول أيهما يستخدم. اختر واحدًا لكل نقطة نهاية. إذا كنت تصمم العقد من الصفر، فإن الأنماط في دليل ترقيم الصفحات في REST API الخاص بنا ستحافظ على اتساق تسمية المعلمات عبر واجهتك.

ماذا يحدث إذا تم حذف صف المؤشر الأساسي؟

مع ترقيم الصفحات بـ keyset، لا شيء يتعطل. لا تتطلب مقارنة WHERE (created_at, id) < (?, ?) وجود الصف الأساسي؛ إنها تبحث عن موضع الحد وتستمر. هذه ميزة حقيقية على تصميمات "المؤشر كبحث عن صف"، وهي بالضبط حالة الحافة التي تستحق التأكيد عليها في سيناريو اختبار Apidog الخاص بك قبل أن يكتشفها المستهلك نيابة عنك.

ممارسة تصميم API في Apidog

اكتشف طريقة أسهل لبناء واستخدام واجهات برمجة التطبيقات