أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

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

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

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

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

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

الرئيسيةالدوراتالعروضالمدونةالدخول

N+1 في GraphQL: DataLoader بيخفّض الاستعلامات من 1200 لـ 8

📅 ٢٦ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
N+1 في GraphQL: DataLoader بيخفّض الاستعلامات من 1200 لـ 8
المستوى المستهدف: متوسط — يفترض أنك كاتب GraphQL resolver قبل كده وعارف ORM (Prisma/Sequelize/TypeORM) بشكل أساسي.

لو الـ GraphQL endpoint عندك بيرجّع قائمة 100 منتج، وكل منتج له category و author، ممكن تلاقي السيرفر بيضرب 201 SQL query في request واحد. ده مش بطء عشوائي، ده نمط اسمه N+1، وله حل اتفق عليه فريق Facebook نفسه اللي صمّم GraphQL: DataLoader. المقال ده بيوريك إزاي تنزل من 1200 query لـ 8 في أقل من 30 سطر كود.

شاشة محرر كود تعرض GraphQL resolvers مع تتبّع تنفيذ استعلامات N+1 على قاعدة البيانات

N+1 في GraphQL: ليه السيرفر بيموت في query بسيط

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

في GraphQL، كل field في الـ schema بيتنفّذ بـ resolver خاص بيه. لو عندك query بترجّع products وكل منتج فيه category، الـ resolver بتاع category بيتنادى مرة لكل منتج. النتيجة: query واحد للقائمة + N استعلام لكل علاقة. ده بالظبط N+1.

الـ trade-off اللي بنشتري بيه مرونة GraphQL هو إن الـ runtime مش شايف الـ query الكاملة زي SQL، فبيشتغل بـ resolver-per-field. لو سبت ده من غير batching، أي لستة بمئة عنصر بتتحوّل لقنبلة استعلامات.

مثال يوضّح N+1 (للمبتدئ)

تخيّل إنك في كافيتيريا. طلبت 100 ساندوتش، وبدل ما الويتر يدخل المطبخ مرة واحدة بقائمة الـ 100، هو بيدخل 100 مرة، كل مرة يقول "ساندوتش واحد لو سمحت". المطبخ شغّال، لكن الويتر هو الـ bottleneck. DataLoader هو الويتر اللي بيقفل ورق الطلبات الـ 100 ويدخل المطبخ مرة واحدة.

تطبيق ده على الكود: من غير DataLoader، 100 منتج بـ category مختلفة بيتحوّلوا لـ 100 استعلام منفصل على جدول categories. مع DataLoader، الـ 100 طلب بيتجمّعوا في SELECT * FROM categories WHERE id IN (1,2,3,...) واحد.

التعريف العلمي بدقة

N+1 query هي حالة بترجع فيها قائمة بطول N، وبعدين بتعمل استعلام إضافي لكل عنصر علشان تجيب علاقة مرتبطة، فينتج عندك N+1 round trip على الـ database. التكلفة الحقيقية مش في وقت تنفيذ الاستعلام، التكلفة في زمن الشبكة (network round-trip latency) ومحدودية الـ connection pool.

DataLoader بيشتغل بـ request-scoped batching + caching. كل resolver بيستدعي loader.load(id)، الـ DataLoader بيجمع كل الـ ids اللي اتطلبت في نفس الـ event loop tick، وبيناديك بـ batchFn واحد بيرجّع كل النتائج معًا. مع كاش داخلي بنفس الـ request علشان لو نفس الـ id اتطلب مرتين، يرجع من الذاكرة.

الحل: كود قابل للنسخ بـ Node.js و Apollo Server

JavaScript
// loaders/categoryLoader.js
import DataLoader from "dataloader";
import { db } from "../db.js";

export function createCategoryLoader() {
  return new DataLoader(async (categoryIds) => {
    const rows = await db.query(
      "SELECT id, name, slug FROM categories WHERE id = ANY($1)",
      [categoryIds]
    );
    const byId = new Map(rows.map((r) => [r.id, r]));
    return categoryIds.map((id) => byId.get(id) ?? null);
  });
}

الترتيب في array الـ output لازم يطابق ترتيب الـ input بالظبط. ده شرط في DataLoader عشان يعرف يربط كل id بنتيجته.

JavaScript
// server.js
import { ApolloServer } from "@apollo/server";
import { createCategoryLoader } from "./loaders/categoryLoader.js";

const server = new ApolloServer({ typeDefs, resolvers });

await startStandaloneServer(server, {
  context: async () => ({
    loaders: {
      category: createCategoryLoader(),
    },
  }),
});

// resolvers.js
export const resolvers = {
  Product: {
    category: (product, _args, ctx) =>
      ctx.loaders.category.load(product.category_id),
  },
};

القاعدة الذهبية: عمر loader واحد لكل request. لو عمّلته على مستوى السيرفر هتسرّب داتا بين users — مشكلة أمنية حقيقية، مش تحسين أداء.

قياس قبل وبعد من بيئة إنتاج فعلية

رسم بياني يقارن عدد استعلامات SQL قبل وبعد استخدام DataLoader في GraphQL مع انخفاض من 1000 إلى 8

السيناريو: متجر إلكتروني، endpoint products(first: 100) بيرجّع المنتج + الكاتيجوري + المؤلف + 5 صور لكل منتج. القياس على PostgreSQL 15، ApolloServer 4، connection pool بحجم 20.

  • قبل: 1 + (100 × 3) + 100 × صورة واحدة = 1201 query، p95 = 1.4 ثانية، DB CPU = 78%.
  • بعد: 1 + 3 batched + 1 batched للصور = 5 إلى 8 queries حسب التكرار، p95 = 92ms، DB CPU = 11%.

الانخفاض في p95 جه أساسًا من تقليل round-trips، مش من تسريع الـ query نفسها. لو الـ DB في نفس الـ data center، round-trip بيتراوح 0.4–1.2ms، اضربها في 1200 وهتفهم 800ms راحت فين.

trade-offs لازم تعرفها قبل ما تنشر الكود

  • الكاش request-scoped فقط. DataLoader مش بديل عن Redis. لو محتاج cache بين requests، استخدم layer منفصل.
  • تكلفة ذاكرة بسيطة. كل request بينشئ Map داخلي. على 10K طلب/دقيقة في نفس اللحظة، ممكن تشوف زيادة 30–60MB heap. مش مشكلة على سيرفر فيه 4GB، لكن ضعها في الحسبان.
  • لازم batchFn يحافظ على الترتيب. أي تغيير في ترتيب النتائج بيخلّي DataLoader يربط الردود بـ ids غلط — bug صعب الاكتشاف.
  • الاستعلام الواحد ممكن يكبر. لو عندك 5000 منتج في request واحد، WHERE id IN (5000 قيمة) ممكن يضرب حد PostgreSQL أو يبطّأ الـ query planner. ضع maxBatchSize: 1000 في إعدادات الـ DataLoader.

متى لا تستخدم DataLoader

  • لو الـ resolver بيرجّع علاقة 1:1 وعندك JOIN في الاستعلام الأصلي خلاص — DataLoader هيضيف layer من غير فايدة.
  • لو بتشتغل في REST مش GraphQL، الحل هنا هو eager loading أو SELECT … JOIN مباشر، مش DataLoader.
  • لو الـ batch size المتوقّع أقل من 5 طلبات في الـ request، التحسين مش هيبان وهتدفع تعقيد كود من غير عائد.
  • لو الـ data source مش بيدعم batching حقيقي (مثلًا API خارجي بيقبل id واحد فقط لكل request)، DataLoader هيخليك تعمل الـ N requests بالتوازي بدل التتابع — تحسين، لكن مش بنفس قوة batching على DB.

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

افتح أكبر GraphQL query عندك في إنتاج، شغّل EXPLAIN على عينة logs أو فعّل query logging لمدة دقيقة. لو شفت نفس الجدول بيتسأل أكتر من 10 مرات في request واحد، عندك N+1. أنشئ createCategoryLoader بالنمط فوق، وسجّل الفرق في p95 قبل وبعد. لو نزل أقل من 200ms، اعتبر المهمة خلصت.

مصادر

  • المستودع الرسمي لـ DataLoader على GitHub من فريق Facebook: https://github.com/graphql/dataloader
  • توثيق Apollo Server حول الـ context والـ batching: https://www.apollographql.com/docs/apollo-server/data/fetching-data
  • مقالة Marc-André Giroux "GraphQL & N+1": https://productionreadygraphql.com/blog/2017-04-09-batching-and-the-n-plus-one-problem
  • توثيق PostgreSQL حول حدود IN و = ANY: https://www.postgresql.org/docs/current/functions-comparisons.html
  • دليل Prisma حول حل N+1 بـ findMany + in: https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance
]]>

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

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

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