تقارير المبيعات في MySQL: GROUP BY و GROUP_CONCAT
صاحب المتجر يريد أن يعرف: من أفضل العملاء؟ أي تصنيف يحقق أعلى متوسط سعر؟ كم طلباً وصلنا كل شهر؟ هذه تقارير، وهي ما يحوّل قاعدة البيانات من مخزن صامت إلى أداة لاتخاذ القرارات. في هذا الدرس الأخير ستبني تقارير حقيقية لمتجرنا، وستتعرف على أدوات تجميع خاصة بـ MySQL.
إجمالي مشتريات كل عميل
Section titled “إجمالي مشتريات كل عميل”SELECT c.name, SUM(o.quantity * p.price) AS total_spentFROM orders oJOIN customers c ON o.customer_id = c.idJOIN products p ON o.product_id = p.idGROUP BY c.id, c.nameORDER BY total_spent DESC;الناتج:
| name | total_spent |
|---|---|
| عمر | 1009.00 |
| سارة | 379.49 |
خطوة بخطوة:
JOINيجمع لكل طلب: العميل والمنتج وسعره.o.quantity * p.priceيحسب قيمة كل طلب.GROUP BY c.id, c.nameيضع طلبات كل عميل في مجموعة.SUMيجمع قيم كل مجموعة.ORDER BY ... DESCيضع الأفضل في الأعلى.
عمر اشترى سماعتين (2 × 450.50) وكتاباً (108.00)، فالإجمالي 1009.00. هذا التقرير هو أساس برامج ولاء العملاء: من ينفق أكثر يستحق خصماً خاصاً.
إحصاءات كل تصنيف
Section titled “إحصاءات كل تصنيف”SELECT category, COUNT(*) AS products_count, ROUND(AVG(price), 2) AS avg_price, SUM(stock) AS total_stockFROM productsGROUP BY category;الناتج:
| category | products_count | avg_price | total_stock |
|---|---|---|---|
| كتب | 2 | 83.25 | 45 |
| إلكترونيات | 2 | 325.25 | 27 |
| أدوات مدرسية | 2 | 157.88 | 50 |
في استعلام واحد: عدد المنتجات، ومتوسط السعر، وإجمالي المخزون لكل تصنيف. (ولاحظ ترتيب التصنيفات حسب تعريف ENUM، كما تعلمت في درس الترتيب.)
GROUP_CONCAT: دمج القيم في نص
Section titled “GROUP_CONCAT: دمج القيم في نص”ميزة خاصة بـ MySQL ومفيدة جداً: بدلاً من عدّ المنتجات، اعرض أسماءها مجمعة:
SELECT category, GROUP_CONCAT(name ORDER BY name SEPARATOR '، ') AS itemsFROM productsGROUP BY category;الناتج:
| category | items |
|---|---|
| كتب | قصص الأطفال، كتاب البرمجة للمبتدئين |
| إلكترونيات | آلة حاسبة علمية، سماعة لاسلكية |
| أدوات مدرسية | حقيبة مدرسية، مجموعة أقلام |
GROUP_CONCAT تجمع قيم المجموعة في نص واحد، ويمكنك تحديد ترتيبها بـ ORDER BY والفاصل بينها بـ SEPARATOR. مثالية لعرض «الوسوم» أو «المنتجات في الطلب» في سطر واحد.
التجميع حسب الوقت
Section titled “التجميع حسب الوقت”كم طلباً كل شهر؟
SELECT YEAR(order_date) AS y, MONTH(order_date) AS m, COUNT(*) AS orders_countFROM ordersGROUP BY YEAR(order_date), MONTH(order_date)ORDER BY y, m;الناتج:
| y | m | orders_count |
|---|---|---|
| 2026 | 10 | 3 |
| 2026 | 11 | 2 |
بدمج دوال التاريخ مع GROUP BY، تبني الرسوم البيانية للمبيعات الشهرية التي تراها في لوحات التحكم. ويمكنك التجميع بالأسبوع أو اليوم بنفس الفكرة.
HAVING: تصفية المجموعات
Section titled “HAVING: تصفية المجموعات”العملاء الذين طلبوا مرتين أو أكثر:
SELECT customer_id, COUNT(*) AS orders_countFROM ordersGROUP BY customer_idHAVING COUNT(*) >= 2;الناتج:
| customer_id | orders_count |
|---|---|
| 1 | 3 |
| 2 | 2 |
تذكّر الفرق: WHERE تصفّي الطلبات قبل التجميع (مثلاً: طلبات أكتوبر فقط)، و HAVING تصفّي المجموعات بعده (مثلاً: العملاء الذين تجاوزوا عدداً معيناً).
الوضع only_full_group_by
Section titled “الوضع only_full_group_by”جرّب هذا الاستعلام:
SELECT category, name FROM products GROUP BY category;الناتج:
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clauseand contains nonaggregated column 'shop.products.name' which is notfunctionally dependent on columns in GROUP BY clause; this is incompatiblewith sql_mode=only_full_group_byلماذا الرفض؟ كل تصنيف فيه منتجان، فأي اسم يجب أن يظهر في صف التصنيف؟ السؤال بلا إجابة منطقية. الإصدارات القديمة من MySQL كانت تختار اسماً عشوائياً بصمت، فتنتج تقارير مضللة. أما الإصدارات الحديثة فتفعّل الوضع only_full_group_by افتراضياً وترفض الاستعلام.
الحل: كل عمود في SELECT إما في GROUP BY، أو داخل دالة تجميع مثل COUNT أو MAX أو GROUP_CONCAT:
SELECT category, GROUP_CONCAT(name) FROM products GROUP BY category;جرّب بنفسك
Section titled “جرّب بنفسك”- اعرض إجمالي الكمية المباعة من كل منتج، مرتبة من الأكثر مبيعاً.
- اعرض إجمالي إيرادات كل شهر (الكمية × السعر).
- اعرض لكل عميل أسماء المنتجات التي اشتراها في سطر واحد بـ
GROUP_CONCAT. - اعرض التصنيفات التي متوسط أسعارها أكبر من 100.
- تحدٍّ: اعرض أكثر منتج مبيعاً في كل شهر.
أخطاء شائعة
Section titled “أخطاء شائعة”- عمود ليس في GROUP BY ولا في دالة تجميع: الخطأ 1055. أضفه لـ
GROUP BYأو ضعه داخل دالة. - شرط على دالة تجميع في WHERE:
WHERE SUM(...) > 100خطأ. استخدمHAVING. - GROUP_CONCAT لنصوص طويلة جداً: النتيجة تُقطع افتراضياً عند 1024 حرفاً. يمكن رفع الحد بالإعداد
group_concat_max_len.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
أي دالة في MySQL تجمع أسماء منتجات كل تصنيف في نص واحد؟
GROUP_CONCAT تدمج قيم المجموعة كلها في نص واحد، أما CONCAT فتدمج قيماً في نفس الصف.
سؤال
لماذا ترفض MySQL الحديثة SELECT category, name FROM products GROUP BY category؟
الوضع only_full_group_by يمنع الأعمدة الغامضة التي قد تعطي نتائج عشوائية مضللة.
الخلاصة
Section titled “الخلاصة”JOINمعGROUP BYوSUMيبني تقارير مثل إجمالي مشتريات كل عميل.COUNTوAVGوSUMمعاً تعطي إحصاءات كاملة لكل مجموعة.GROUP_CONCATميزة MySQL لدمج قيم المجموعة في نص، مع ترتيب وفاصل.- دوال التاريخ مع
GROUP BYتبني تقارير شهرية، وHAVINGتصفّي المجموعات. only_full_group_byيمنع الأعمدة الغامضة.