كيفية استخدام ‎JOIN‎ و‎STRING_AGG()‎ في ‎Microsoft SQL Server

دقائق القراءة: 7

مقدمة عملية إلى الربط وتجميع النصوص في Microsoft SQL Server

في هذا الدليل سنتعرّف على طريقة استخدام JOIN بين أكثر من جدول، ثم تجميع النتائج في صف واحد لكل موظف باستخدام الدالة STRING_AGG() داخل Microsoft SQL Server. الهدف ليس فقط تنفيذ الاستعلام، بل فهم المنطق الذي يقف وراء اختيار نوع الربط المناسب، وكيفية التعامل مع نتائج GROUP BY دون الوقوع في الأخطاء الشائعة.

إذا كانت لديك خبرة سابقة مع أنظمة إدارة قواعد البيانات العلائقية مثل MySQL أو PostgreSQL، فستجد أن العمل مع SQL Server مفهوم وسلس إلى حد كبير.

شرح استخدام أوامر الربط وتجميع النصوص في مايكروسوفت SQL Server

ما هو Microsoft SQL Server؟

Microsoft SQL Server هو نظام إدارة قواعد بيانات علائقية RDBMS صُمم لتخزين البيانات وتنظيمها ومعالجتها بكفاءة. تعتمد عليه الشركات والتطبيقات لإدارة كميات كبيرة من البيانات، وإنشاء استعلامات دقيقة، وبناء تقارير موثوقة تدعم اتخاذ القرار.

تكمن قوته في قدرته على:

  • تخزين البيانات بطريقة منظمة وقابلة للتوسع.
  • تنفيذ استعلامات معقدة على عدة جداول.
  • ضمان تكامل العلاقات بين الكيانات المختلفة.
  • دعم الدوال التجميعية والتحليلية المتقدمة.

المشكلة: كيف نستخرج بيانات الموظفين مع مشاريعهم؟

لنفترض أن لدينا ثلاثة جداول رئيسية:

  • Employee
  • Project
  • EmployeeProject

يمثل الجدول EmployeeProject العلاقة بين الموظفين والمشاريع، أي أنه جدول ربط Bridge Table أو Junction Table.

مخطط قاعدة البيانات العلائقية الذي يوضح العلاقة بين الموظفين والمشاريع وجدول الربط

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

جدول الموظفين في SQL Serverجدول الربط بين الموظفين والمشاريع في SQL Serverجدول المشاريع في SQL Server

الهدف الأساسي هنا هو استرجاع جميع الموظفين من جدول Employee سواء كانوا مرتبطين بمشاريع أم لا.

فهم منطق الربط بين ثلاثة جداول

لحل هذه المسألة، نحتاج إلى تنفيذ الربط على مرحلتين:

  1. ربط Employee مع EmployeeProject.
  2. ربط الناتج مع جدول Project.

نوع الربط المستخدم في كل خطوة يغيّر النتيجة النهائية جذرياً، ولهذا سنراجع أكثر من سيناريو.

الحل الأول: استخدام INNER JOIN

إذا استخدمنا INNER JOIN، فسنحصل فقط على السجلات التي لها تطابق فعلي في الجدولين.

SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
FROM Employee AS e
INNER JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId

هذا الاستعلام يعرض الموظفين الذين لديهم ارتباط فعلي داخل جدول EmployeeProject.

نتيجة استخدام INNER JOIN بين جدول الموظفين وجدول الربط

بعد ذلك يمكننا ربط الناتج بجدول Project للوصول إلى أسماء المشاريع:

SELECT abc.FirstName, abc.LastName, abc.City, abc.Designation, p.Name AS Project
FROM Project AS p
INNER JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    INNER JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId

نتيجة استخدام INNER JOIN على ثلاثة جداول في SQL Server

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

الحل الثاني: استخدام LEFT JOIN ثم RIGHT JOIN

الخطوة الأولى: الاحتفاظ بجميع الموظفين

حتى نضمن ظهور كل الموظفين، نبدأ من جدول Employee ونستخدم LEFT JOIN مع جدول EmployeeProject:

SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
FROM Employee AS e
LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId

نتيجة استخدام LEFT JOIN لإظهار جميع الموظفين حتى بدون مشاريع

هنا نلاحظ أن الموظف الذي لا يملك مشروعاً ما يزال ظاهراً، بينما تكون قيمة ProjectId لديه مساوية لـ NULL.

الخطوة الثانية: جلب أسماء المشاريع مع الحفاظ على جميع الموظفين

بعد الحصول على كل الموظفين، نربط الناتج مع جدول Project بحيث لا نفقد أي صف من الناتج السابق:

SELECT abc.FirstName, abc.LastName, abc.City, abc.Designation, p.Name AS Project
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId

نتيجة الربط التي تعرض جميع الموظفين مع المشاريع المرتبطة بهم إن وجدت

بهذا نكون قد حققنا الهدف الأساسي: عرض جميع الموظفين سواء كانوا مرتبطين بمشروع أم لا.

لماذا لا يكفي GROUP BY وحده؟

قد نرغب الآن في تحسين الناتج ليصبح هناك صف واحد فقط لكل موظف بدلاً من تكرار الموظف في عدة صفوف عند امتلاكه أكثر من مشروع.

قد تبدو فكرة استخدام GROUP BY مباشرة منطقية، لكن يجب الانتباه إلى قاعدة مهمة:

  • أي عمود موجود في جملة SELECT يجب أن يكون إما داخل جملة GROUP BY أو داخل دالة تجميعية مثل SUM() أو MAX() أو COUNT().

مثلاً، إذا كتبنا:

SELECT abc.FirstName, abc.LastName, abc.City, abc.Designation, p.Name AS Project
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName

فسيظهر خطأ، لأن الأعمدة الأخرى مثل LastName وCity وDesignation ليست ضمن GROUP BY وليست داخل دوال تجميعية.

خطأ GROUP BY في SQL Server عند تحديد أعمدة غير مجمعة أو غير خاضعة لدالة تجميعية

مثال مبسط على التجميع الصحيح

إذا اخترنا فقط الاسم الأول وقمنا بالتجميع عليه، فلن تكون هناك مشكلة:

SELECT abc.FirstName
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName

مثال صحيح لاستخدام GROUP BY على عمود واحد في SQL Server

لكن عند اختيار FirstName وLastName مع التجميع على FirstName فقط، تظهر المشكلة بسبب الغموض. قد يوجد أكثر من موظف يحمل الاسم الأول نفسه مع ألقاب مختلفة.

SELECT abc.FirstName, abc.LastName
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName

مثال على الغموض الناتج عن استخدام GROUP BY مع الاسم الأول فقط

كيف نعالج الغموض في GROUP BY؟

هناك طريقتان شائعتان:

  1. إضافة كل الأعمدة المطلوبة إلى GROUP BY.
  2. استخدام دالة تجميعية على بعض الأعمدة مثل MAX().

الخيار الأول:

SELECT abc.FirstName, abc.LastName
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName, abc.LastName

حل مشكلة GROUP BY بإضافة الاسم الأول والأخير معاً إلى التجميع

الخيار الثاني:

SELECT abc.FirstName, MAX(abc.LastName) AS LastName
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName

حل بديل لمشكلة GROUP BY باستخدام الدالة التجميعية MAX

رغم أن الحل الثاني قد يعمل تقنياً، فإن الحل الأول غالباً أكثر منطقية ووضوحاً في هذا النوع من البيانات.

لماذا لا نكتفي بتجميع كل الأعمدة؟

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

SELECT abc.FirstName, abc.LastName, abc.City, abc.Designation, p.Name AS Project
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName, abc.LastName, abc.City, abc.Designation, p.Name

نتيجة تجميع كل الأعمدة بما فيها اسم المشروع في SQL Server

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

الحل الأفضل: استخدام STRING_AGG() لإرجاع صف واحد لكل موظف

هنا تظهر فائدة الدالة STRING_AGG()، إذ تسمح لنا بدمج قيم متعددة من عمود واحد داخل نص واحد مفصول بفاصل نختاره، مثل الفاصلة ,.

SELECT abc.FirstName, abc.LastName, abc.Designation,
       STRING_AGG(p.Name, ',') WITHIN GROUP (ORDER BY p.Name) AS Project
FROM Project AS p
RIGHT JOIN (
    SELECT e.Id, e.FirstName, e.LastName, e.Designation, e.City, ep.ProjectId
    FROM Employee AS e
    LEFT JOIN EmployeeProject AS ep ON e.Id = ep.EmployeeId
) AS abc ON p.Id = abc.ProjectId
GROUP BY abc.FirstName, abc.LastName, abc.City, abc.Designation

نتيجة استخدام STRING_AGG لتجميع أسماء المشاريع في صف واحد لكل موظف

بهذا الاستعلام نحصل على:

  • صف واحد لكل موظف.
  • جميع بيانات الموظف الأساسية.
  • قائمة المشاريع في حقل واحد.

ما الذي يفعله هذا الاستعلام بالضبط؟

  • LEFT JOIN يضمن بقاء جميع الموظفين في النتيجة.
  • RIGHT JOIN مع الناتج الوسيط يساعد على الاحتفاظ بكل صفوف الموظفين عند الربط مع المشاريع.
  • GROUP BY يجمع الصفوف المتشابهة حسب بيانات الموظف.
  • STRING_AGG() يدمج أسماء المشاريع المتعددة في قيمة نصية واحدة.
  • الجزء WITHIN GROUP (ORDER BY p.Name) يرتب أسماء المشاريع داخل النص الناتج ترتيباً أبجدياً.

أفضل الممارسات عند استخدام JOIN وSTRING_AGG()

  • ابدأ دائماً من الجدول الذي تريد الاحتفاظ بجميع سجلاته، ثم اختر نوع الربط المناسب.
  • قبل استخدام GROUP BY، حدّد بوضوح ما إذا كنت تريد صفاً لكل كيان أم صفاً لكل علاقة.
  • استخدم الدوال التجميعية فقط عندما يكون معناها التجاري واضحاً، وليس فقط لإسكات الخطأ.
  • لفهم النتائج بدقة، جرّب الاستعلام على مراحل: أولاً الربط، ثم التصفية، ثم التجميع.
  • عند التعامل مع أسماء أو نصوص متعددة، تعد STRING_AGG() خياراً ممتازاً لعرض النتائج بشكل أنظف وأسهل للقراءة.

متى يكون استخدام STRING_AGG() مفيداً؟

لا يقتصر استخدام STRING_AGG() على المشاريع والموظفين فقط، بل يفيد في كثير من السيناريوهات العملية، مثل:

  • جمع الصلاحيات المرتبطة بمستخدم واحد.
  • عرض الوسوم Tags المرتبطة بمقال في حقل واحد.
  • دمج أسماء الفروع المرتبطة بعميل.
  • إخراج تقارير إدارية بصيغة أكثر اختصاراً ووضوحاً.

الخلاصة التقنية

إذا كان هدفك في Microsoft SQL Server هو عرض جميع السجلات من جدول أساسي مع البيانات المرتبطة من جداول أخرى، فإن فهم الفرق بين INNER JOIN وLEFT JOIN وRIGHT JOIN يعد خطوة محورية. أما عندما تحتاج إلى تقليص النتائج إلى صف واحد لكل كيان مع دمج القيم النصية المرتبطة، فإن الدالة STRING_AGG() تقدم حلاً عملياً وأنيقاً. تقنياً، الجمع بين الربط الصحيح والتجميع الذكي يمنحك استعلامات أدق، ونتائج أوضح، وقابلية أعلى لبناء تقارير احترافية قابلة للاستخدام في التطبيقات ولوحات التحكم.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *