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

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

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

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

الدعم

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

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

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

Keyset Pagination للمحترف: ليه OFFSET 100000 بياخد 850ms والـ Seek بياخد 2ms

محترف٢٠ يونيو ٢٠٢٦5 دقائق قراءة
Keyset Pagination للمحترف: ليه OFFSET 100000 بياخد 850ms والـ Seek بياخد 2ms

مستوى المقال: محترف — الكلام ده موجّه لمطور عنده تجربة مع SQL و EXPLAIN ANALYZE، وبيدير جداول فيها ملايين الصفوف. لو لسه بتبدأ مع قواعد البيانات، الفكرة هتوصلك من المثال البسيط في الأول، بس التطبيق الكامل يحتاج خلفية في الـ indexing.

ليه OFFSET بيبطّئ صفحاتك الأخيرة، والحل في سطر واحد

لو صفحة 5000 في الـ API بتاعتك بتاخد ثانية كاملة بينما صفحة 1 بترجع في 2 مللي ثانية، المشكلة مش في السيرفر ولا في غياب الـ index. المشكلة في OFFSET نفسه. لو غيّرت أسلوب الترقيم لـ Keyset (المعروف بالـ Seek Method)، أي صفحة هتتحمّل في زمن ثابت تقريبًا مهما كان رقمها.

مخطط يقارن بين OFFSET 100000 الذي يقرأ 100,020 صف في 850 مللي ثانية وبين Keyset Seek الذي يقرأ 20 صف في 2 مللي ثانية على PostgreSQL

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

تخيّل دليل تليفونات ورقي مرتّب بالاسم. لو طلبت منك الاسم رقم 100,020، إنت مش هتقدر تقفز له على طول. هتعدّ من الأول: واحد، اتنين، تلاتة... لحد ما توصل للرقم 100,000، وبعدها تبدأ تقرأ العشرين اسم اللي إنت عايزهم. الـ 100,000 اسم اللي عدّيت عليهم كانوا شغل ضايع بالكامل.

LIMIT 20 OFFSET 100000 بيعمل بالظبط ده. قاعدة البيانات لازم تولّد وتعدّ 100,000 صف قبل المطلوب، ترميهم، وترجّع العشرين الباقيين. الافتراض إن عندك جدول بـ 2 مليون صف ومستخدم بيتصفّح بعمق، التكلفة بتكبر خطّيًا مع رقم الصفحة. ده اللي بيخلّي الصفحات الأخيرة بطيئة بينما الأولى سريعة.

ليه OFFSET بطيء فعلًا (الشرح الدقيق)

الـ index من نوع B-tree في PostgreSQL مرتّب ويقدر يقفز لأي قيمة بحثًا عنها في زمن لوغاريتمي. لكن OFFSET مش قيمة يبحث عنها، هو عدّاد صفوف. المُحرّك مفيش عنده طريقة يعرف بيها "وين الصف رقم 100,000" غير إنه يمشي على الصفوف المرتّبة واحد واحد ويعدّهم. ده اللي بيسمّوه في خطة التنفيذ تكلفة على شكل rows removed by offset.

SQL
-- جدول الطلبات: 2 مليون صف، index على id
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
ORDER BY id
LIMIT 20 OFFSET 100000;

-- النتيجة المختصرة:
-- Limit  (actual time=842.105..842.140 rows=20)
--   ->  Index Scan using orders_pkey on orders
--         (actual rows=100020 loops=1)
-- Execution Time: 842.318 ms

لاحظ actual rows=100020. المُحرّك قرأ 100,020 صف فعلًا عشان يرمي منهم 100,000. ده مش index بطيء، ده استخدام غلط للـ index. والمشكلة بتتضاعف مع كل صفحة أعمق.

الحل: Keyset / Seek Method

الفكرة بسيطة: بدل ما تقول "تخطّ أول 100,000 صف"، قول "ابدأ من بعد آخر صف شفته". إنت بتحوّل الترقيم من عدّاد لـ شرط بحث، والشرط ده الـ index بيخدمه مباشرة.

  1. رتّب على عمود (أو أعمدة) فريد ومُفهرَس، زي id أو (created_at, id).
  2. في كل استجابة، خزّن قيمة آخر صف (الـ cursor) ورجّعها للـ frontend.
  3. الطلب اللي بعده يبعت الـ cursor، وإنت تستخدمه في شرط WHERE بدل OFFSET.
SQL
-- الصفحة الأولى
SELECT * FROM orders
ORDER BY id
LIMIT 20;

-- الصفحات اللي بعدها: مرّر آخر id شفته (مثلاً 100000)
SELECT * FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 20;

-- EXPLAIN: actual rows=20 فقط، Execution Time: ~2 ms

لو الترتيب على عمود مش فريد (زي created_at اللي ممكن يتكرر)، استخدم مقارنة مركّبة عشان متفقدش ولا صف ومتكرّرش حد:

SQL
SELECT * FROM orders
WHERE (created_at, id) > ('2026-06-19 10:00:00', 100000)
ORDER BY created_at, id
LIMIT 20;
-- ويفضّل index: CREATE INDEX ON orders (created_at, id);

القياس على جدول 2 مليون صف: OFFSET عند صفحة 5000 وصل لـ 900 مللي ثانية، بينما Keyset فضل ثابت حوالي 2.2 مللي ثانية على نفس الصفحة. الفرق مش نسبة ثابتة، هو بيكبر كل ما المستخدم يغوص أعمق.

رسم بياني خطي يقارن زمن استعلام OFFSET الذي يرتفع من 1.9 إلى 900 مللي ثانية مع تقدم رقم الصفحة مقابل Keyset الذي يبقى ثابتاً عند نحو 2 مللي ثانية على جدول 2 مليون صف في PostgreSQL 16

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

  • القفز العشوائي للصفحات بيتكسر. بتكسب زمن ثابت، بتخسر القدرة تروح لـ "صفحة 5000" مباشرة. Keyset بيدعم "التالي/السابق" مش "اقفز لرقم". لو عندك pagination بأرقام صفحات في الـ UI، ده تغيير في تجربة المستخدم.
  • الترتيب لازم يكون فريد وحتمي. لو رتّبت على عمود غير فريد من غير tie-breaker زي id، ممكن تفقد أو تكرّر صفوف عند حدود الصفحات.
  • الـ index لازم يطابق الترتيب. الترتيب في ORDER BY لازم يكون مغطّى بـ index بنفس الأعمدة ونفس الاتجاه، وإلا هترجع لنفس البطء.
  • العدّ الكلي (total count) منفصل. Keyset مبيديكش رقم الصفحات الإجمالي ببلاش. لو محتاج العدد، احسبه باستعلام منفصل أو بتقدير عبر reltuples.

متى لا تستخدم Keyset Pagination

متستخدمهوش لو الجدول صغير (آلاف قليلة من الصفوف) — هنا OFFSET أبسط وفرق الأداء مش محسوس، والتعقيد الزيادة مش مبرّر. كمان متستخدمهوش لو المستخدم محتاج فعلًا يقفز لأرقام صفحات عشوائية أو عايز ترتيب متغيّر بشكل ديناميكي على أعمدة غير مُفهرَسة. وفي الـ data exports الكبيرة، فكّر في cursor على مستوى الـ DB أو COPY بدل أي pagination في طبقة التطبيق.

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

افتح أبطأ endpoint فيه list بيستخدم OFFSET، وشغّل عليه EXPLAIN (ANALYZE, BUFFERS) على صفحة عميقة. لو شفت actual rows أكبر بكتير من الـ LIMIT، ده تأكيد إن OFFSET بيقرا ويرمي. حوّله لـ Keyset على عمود مُفهرَس وفريد، وقيس الفرق. لو الزمن مفضلش ثابت بعد التحويل، غالبًا الـ ORDER BY مش مغطّى بالـ index الصح.

مصادر

  • توثيق PostgreSQL الرسمي — LIMIT and OFFSET، وملاحظتهم إن الصفوف اللي يتخطّاها OFFSET لازم تُحسب داخليًا (postgresql.org/docs).
  • توثيق PostgreSQL — Indexes and ORDER BY حول مطابقة الـ index لاتجاه الترتيب (postgresql.org/docs/current/indexes-ordering.html).
  • Markus Winand — "No Offset" و use-the-index-luke.com، المرجع الأشهر في شرح الـ Seek Method وحدود OFFSET.
  • أرقام الزمن في المقال تقديرية ومقاسة عبر EXPLAIN ANALYZE على جدول اختباري بـ 2 مليون صف على PostgreSQL 16، وتختلف حسب العتاد وحجم الصف والـ caching.

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

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

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