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

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

ليه OFFSET 100000 بيخلّي استعلامك يزحف؟ الترقيم بالـ Keyset

متوسط20 يوليو 20265 دقائق قراءة
ليه OFFSET 100000 بيخلّي استعلامك يزحف؟ الترقيم بالـ Keyset

المستوى: متوسط — للمطوّرين اللي بيعملوا pagination على جداول فيها مئات الآلاف لملايين الصفوف.

لو الصفحة رقم 1 في الـ API بترجع في 5 مللي ثانية، والصفحة رقم 10000 بتاخد قرب الثانية على نفس البيانات، المشكلة مش في السيرفر ولا في الشبكة. المشكلة في إنك بتستخدم OFFSET. في المقال ده هتعرف ليه OFFSET بيبطأ كل ما رقم الصفحة يكبر، وإزاي تحوّله لترقيم بالـ Keyset يفضل ثابت السرعة مهما وصلت.

المشكلة باختصار

الترقيم بالإزاحة (LIMIT ... OFFSET ...) هو الطريقة الافتراضية في أغلب الـ ORM. وهو تمام على أول كام صفحة. لكن على جدول كبير، الصفحات البعيدة بتتحول لعبء حقيقي على قاعدة البيانات. الافتراض هنا إن عندك جدول بملايين الصفوف، والمستخدمين بيوصلوا لصفحات عميقة (بحث، سكرول لا نهائي، تصدير بيانات).

ليه OFFSET بيزحف؟ (المفهوم الأول بمثال)

تخيّل معاك كتاب فيه مليون اسم مرتّبين، وطلبت منك الأسماء من رقم 900001 لـ 900010. الطريقة السريعة إنك تفتح على الاسم اللي وقفت عنده المرة اللي فاتت وتكمّل. لكن OFFSET بيعمل حاجة تانية: بيبدأ يعدّ من أول الكتاب اسم اسم، يرمي أول 900000، وبعدها بس يبدأ يديك العشرة اللي انت عايزهم.

ده بالظبط اللي بيحصل فعلاً في قاعدة البيانات. SELECT ... LIMIT 10 OFFSET 900000 بيقرأ 900010 صف من الـ index أو الجدول، يرمي أول 900000، ويرجّع آخر 10. يعني الشغل بيزيد خطيًا مع رقم الصفحة. الصفحة الأخيرة بتكلّف أضعاف الصفحة الأولى، رغم إن عدد النتايج واحد.

الحل: الترقيم بالـ Keyset (Seek Method)

بدل ما تقول "تخطّى 900000 صف"، قول "هاتلي الصفوف اللي مفتاحها بعد آخر صف شفته". ده الفرق الجوهري: بنستخدم قيمة (cursor) من الصفحة السابقة، والـ index بيقفز عليها مباشرة من غير ما يعدّ حاجة.

  1. رتّب على عمود مفهرس وفريد (زي id) أو تركيبة أعمدة تضمن ترتيبًا ثابتًا.
  2. خزّن قيمة العمود ده من آخر صف في الصفحة الحالية، وابعتها كـ cursor للصفحة اللي بعدها.
  3. في الصفحة الجديدة، استخدم WHERE على المفتاح بدل OFFSET.
SQL
-- البطيء: بيقرأ مليون صف ويرميهم قبل ما يوصل لنتيجتك
SELECT id, title, created_at
FROM articles
ORDER BY id
LIMIT 10 OFFSET 1000000;

-- السريع (Keyset): بيقفز بالـ index على آخر id شُفته
SELECT id, title, created_at
FROM articles
WHERE id > 1000000        -- آخر id من الصفحة السابقة (الـ cursor)
ORDER BY id
LIMIT 10;

-- شرط أساسي: index على عمود الترتيب
CREATE INDEX idx_articles_id ON articles (id);

لو بترتّب على عمود مش فريد (زي created_at اللي ممكن يتكرر)، لازم تضيف عمود فاصل (tie-breaker) عشان متفوّتش صفوف ولا تكرّرها:

SQL
-- ترتيب تنازلي على تاريخ الإنشاء مع id كفاصل
SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 10;

-- والـ index المركّب اللي بيخلّي ده رخيص
CREATE INDEX idx_articles_created_id ON articles (created_at DESC, id DESC);

الأرقام: قبل وبعد

على جدول اختباري فيه 10 ملايين صف في PostgreSQL، وبفهرس على عمود الترتيب، القياس التقريبي بـ EXPLAIN ANALYZE بيطلع كالتالي (الأرقام تقديرية وبتختلف حسب الهاردوير و الـ cache، لكن النسبة ثابتة):

  • LIMIT 10 OFFSET 1000000: حوالي 900 لـ 1300 مللي ثانية، لأنه بيمرّ على أكتر من مليون صف.
  • Keyset بـ WHERE id > :last: حوالي 1 لـ 5 مللي ثانية، لأنه index seek على 10 صفوف بس.

يعني تحسّن في حدود 200 لـ 600 ضعف على الصفحات العميقة. وأهم من الرقم نفسه: زمن الـ Keyset ثابت تقريبًا سواء كنت في الصفحة 5 أو 50000، بينما الـ OFFSET بيسوء خطيًا.

الـ trade-offs اللي لازم تعرفها

مفيش حل ببلاش. الـ Keyset بيكسّبك زمن شبه ثابت واستهلاك أقل للـ CPU على الجداول الكبيرة. بتخسر مقابله حاجتين:

  • مش قادر تقفز لصفحة برقم عشوائي. مفيش "روح لصفحة 500" مباشرة، لأنك محتاج الـ cursor بتاع الصفحة اللي قبلها. بيشتغل حلو مع "التالي/السابق" والسكرول اللا نهائي، مش مع أرقام صفحات.
  • حساب إجمالي الصفحات مش مجاني. عرض "صفحة 3 من 40000" بيحتاج COUNT منفصل، وده هو نفسه غالي على الجداول الكبيرة.
  • الترتيب لازم يكون deterministic. لو العمود مش فريد، لازم فاصل (tie-breaker)، وإلا هتفوّت أو تكرّر صفوف عند الحدود.

متى لا تستخدم هذه الطريقة

الـ Keyset مش دايمًا الأنسب. سيبك من التعقيد ده في الحالات دي:

  • الجدول صغير (بضعة آلاف صف)؛ الفرق مش محسوس والـ OFFSET أبسط.
  • الـ UX محتاج قفز صريح لأرقام صفحات (زي جدول إداري بترقيم 1..2..50).
  • مفيش عمود ترتيب ثابت ومفهرس، أو الترتيب بيتغير مع كل طلب (زi ترتيب عشوائي).

الخطوة التالية

افتح أبطأ endpoint فيه pagination عندك، شغّل عليه EXPLAIN ANALYZE على أعمق صفحة، وسجّل الزمن. بعدها حوّل الاستعلام لـ Keyset بشرط WHERE على المفتاح المفهرس، وقِس تاني. المفروض تشوف الزمن نزل لأجزاء من الثانية وبقى ثابت مهما غُرت في العمق.

المصادر

  • PostgreSQL Documentation — LIMIT and OFFSET: postgresql.org/docs/current/queries-limit.html
  • Markus Winand — "We need tool support for keyset pagination" (No Offset): use-the-index-luke.com/no-offset
  • Use The Index, Luke — Fetch Next Page (Seek Method): use-the-index-luke.com/sql/partial-results/fetch-next-page
  • MySQL Reference Manual — LIMIT Query Optimization: dev.mysql.com/doc/refman/8.0/en/limit-optimization.html
  • Slack Engineering — Evolving API Pagination at Slack: slack.engineering/evolving-api-pagination-at-slack

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة