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

ما هو Microsoft SQL Server؟
Microsoft SQL Server هو نظام إدارة قواعد بيانات علائقية RDBMS صُمم لتخزين البيانات وتنظيمها ومعالجتها بكفاءة. تعتمد عليه الشركات والتطبيقات لإدارة كميات كبيرة من البيانات، وإنشاء استعلامات دقيقة، وبناء تقارير موثوقة تدعم اتخاذ القرار.
تكمن قوته في قدرته على:
- تخزين البيانات بطريقة منظمة وقابلة للتوسع.
- تنفيذ استعلامات معقدة على عدة جداول.
- ضمان تكامل العلاقات بين الكيانات المختلفة.
- دعم الدوال التجميعية والتحليلية المتقدمة.
المشكلة: كيف نستخرج بيانات الموظفين مع مشاريعهم؟
لنفترض أن لدينا ثلاثة جداول رئيسية:
EmployeeProjectEmployeeProject
يمثل الجدول EmployeeProject العلاقة بين الموظفين والمشاريع، أي أنه جدول ربط Bridge Table أو Junction Table.

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



الهدف الأساسي هنا هو استرجاع جميع الموظفين من جدول Employee سواء كانوا مرتبطين بمشاريع أم لا.
فهم منطق الربط بين ثلاثة جداول
لحل هذه المسألة، نحتاج إلى تنفيذ الربط على مرحلتين:
- ربط
EmployeeمعEmployeeProject. - ربط الناتج مع جدول
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.

بعد ذلك يمكننا ربط الناتج بجدول 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 ليس الخيار الأفضل عندما نريد جميع الموظفين دون استثناء.
الحل الثاني: استخدام 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

هنا نلاحظ أن الموظف الذي لا يملك مشروعاً ما يزال ظاهراً، بينما تكون قيمة 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 وليست داخل دوال تجميعية.

مثال مبسط على التجميع الصحيح
إذا اخترنا فقط الاسم الأول وقمنا بالتجميع عليه، فلن تكون هناك مشكلة:
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

لكن عند اختيار 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. - استخدام دالة تجميعية على بعض الأعمدة مثل
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

الخيار الثاني:
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

رغم أن الحل الثاني قد يعمل تقنياً، فإن الحل الأول غالباً أكثر منطقية ووضوحاً في هذا النوع من البيانات.
لماذا لا نكتفي بتجميع كل الأعمدة؟
إذا قمنا بتجميع جميع الأعمدة، بما فيها اسم المشروع، فسنعود تقريباً إلى النتيجة نفسها التي تحتوي على عدة صفوف للموظف الواحد، لأن كل مشروع سيجعل الصف مميزاً عن غيره.
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

إذن، نحن بحاجة إلى أسلوب مختلف: تجميع بيانات الموظف كصف واحد، ثم دمج أسماء المشاريع في حقل نصي واحد.
الحل الأفضل: استخدام 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

بهذا الاستعلام نحصل على:
- صف واحد لكل موظف.
- جميع بيانات الموظف الأساسية.
- قائمة المشاريع في حقل واحد.
ما الذي يفعله هذا الاستعلام بالضبط؟
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() تقدم حلاً عملياً وأنيقاً. تقنياً، الجمع بين الربط الصحيح والتجميع الذكي يمنحك استعلامات أدق، ونتائج أوضح، وقابلية أعلى لبناء تقارير احترافية قابلة للاستخدام في التطبيقات ولوحات التحكم.