ما الذي تفعله دالة التجميع فعليًا؟
معظم دوال SQL التي تعرّفت عليها حتى الآن تعمل صفًا صفًا: UPPER(name) تُستدعى مرة واحدة لكل صف، وROUND(price, 2) كذلك تُستدعى مرة واحدة لكل صف. أما دوال التجميع في SQLite فمختلفة تمامًا؛ فهي تنظر إلى مجموعة من الصفوف ككل وتختصرها إلى قيمة واحدة.
لنُجهّز جدولًا صغيرًا نتسلّى به:
خمسة صفوف تدخل، صف واحد يخرج. هذه هي الفكرة الأساسية باختصار: دوال التجميع في SQLite تضغط مجموعة صفوف وتُلخّصها في صفّ واحد. وعندما لا تستخدم GROUP BY، فإن هذا التلخيص يشمل كل صفوف النتيجة دفعةً واحدة.
دالة COUNT: عدّ الصفوف أم عدّ القيم؟
تأتي دالة COUNT بثلاث صيغ مختلفة، والفرق بينها مهم جدًا:
COUNT(*)يحسب عدد الصفوف، بما فيها الصفوف التي تحتوي علىNULL، ويُرجِع دائمًا رقمًا.COUNT(column)يحسب القيم غير الفارغة (non-NULL) في العمود المحدد.COUNT(DISTINCT column)يحسب القيم الفريدة وغير الفارغة فقط.
عندنا خمسة صفوف، ثلاثة منها تحتوي على قيمة في amount، وثلاثة عملاء مختلفين. فإذا لاحظت يومًا أن COUNT(amount) أقل من COUNT(*) وتساءلت عن السبب، فالجواب ببساطة: قيم NULL لا تدخل في الحساب.
دوال SUM و AVG و MIN و MAX
دوال التجميع الحسابية تعمل كما تتوقع تمامًا، مع قاعدة واحدة هادئة لكنها مهمة: جميعها تتجاهل قيم NULL:
AVG تساوي (10 + 20 + 30) / 3 = 20.0، وليست 60 / 4 = 15.0. فالمقام هنا هو عدد القيم غير الـ NULL فقط. إذا كان هذا السلوك لا يناسبك، وأردت اعتبار القيم المفقودة أصفارًا، فلا بدّ من التصريح بذلك بشكل واضح:
MIN و MAX يشتغلان كذلك مع النصوص والتواريخ، فالمقارنة تتم معجمياً (lexicographically) في حالة النصوص، وكسلاسل ISO في حالة التواريخ بالصيغة القياسية.
الفرق بين SUM و TOTAL
يوفّر SQLite دالة تجميع ثانية شبيهة بـ SUM وهي TOTAL، تعالج مشكلتين مزعجتين في SUM:
- الدالة
SUMعلى صفر صفوف تُرجعNULL، أماTOTALفترجع0.0. - الدالة
SUMعلى قيم كلهاNULLتُرجعNULL، بينماTOTALترجع0.0. - الدالة
TOTALترجع دائمًا رقمًا عشريًا، ولذلك لا تقع في فخ تجاوز سعة الأعداد الصحيحة.
لكن لكل ميزة ثمن: TOTAL ليست جزءًا من معيار SQL، كما أن إرجاعها لقيمة من نوع REAL دائمًا قد يفاجئك إن كنت تتوقع عددًا صحيحًا. استخدمها حين يكون المنطق المناسب لتطبيقك هو "غياب الصفوف يعني صفرًا"، والتزم بـ SUM حين تريد السلوك المعياري لـ SQL.
استخدام DISTINCT داخل دوال التجميع
ليست COUNT وحدها التي تقبل DISTINCT، بل تستطيع استخدامها داخل أي دالة تجميع. وظيفتها هي حذف القيم المكرّرة قبل تنفيذ عملية التجميع:
SUM(amount) يجمع قيمة amount لكل الصفوف، بينما SUM(DISTINCT amount) يجمع كل قيمة فريدة مرة واحدة فقط — وهذا مفيد في حالات مثل "مجموع قيم الفواتير الفريدة"، لكنه نادراً ما يكون ما تحتاجه فعلاً. الأكثر استخداماً هو COUNT(DISTINCT customer).
شرح FILTER clause في SQLite: تجميع جزء من الصفوف
حين تريد تطبيق دالة التجميع على بعض الصفوف فقط، يكون أول ما يخطر ببالك هو WHERE. لكن المشكلة أن WHERE يُصفّي كل شيء دفعة واحدة، فلا يمكنك مثلاً أن تحسب "عدد الطلبات المدفوعة" و"عدد المرتجعات" في نفس الاستعلام بهذه الطريقة. هنا يأتي دور FILTER:
كل عبارة FILTER (WHERE ...) تُطبَّق على دالة التجميع المرتبطة بها فقط. مرور واحد على الجدول، ومع ذلك تحصل على ملخصات لشرائح متعددة. قبل ظهور FILTER، كان المطورون يلجؤون إلى SUM(CASE WHEN status = 'paid' THEN amount END) — نفس الفكرة تمامًا، لكن بكتابة أطول.
دالة GROUP_CONCAT: دمج النصوص في SQLite
تُعدّ GROUP_CONCAT الاستثناء بين دوال التجميع في SQLite. فبدلًا من أن تُعيد رقمًا، تقوم بدمج القيم في سلسلة نصية واحدة:
الفاصل الافتراضي هو الفاصلة. مرّر وسيطًا ثانيًا لو حبيت تستخدم فاصلًا مختلفًا. الترتيب غير مضمون ما لم تكتب الاستدعاء بالشكل GROUP_CONCAT(tag ORDER BY tag) — وهذي حيلة مفيدة لمّا يظهر الناتج في واجهة المستخدم وتبي ترتيبه ثابتًا.
استخدام دوال التجميع بدون GROUP BY
كل الأمثلة التي مرّت معنا واستعملت دوال التجميع بدون GROUP BY رجّعت صفًا واحدًا فقط. وهذي هي القاعدة: أي SELECT فيه دالة تجميع وما فيه GROUP BY بيعطيك ملخصًا من صف واحد لكامل الجدول (بعد تطبيق WHERE).
وتقدر تخلط بين أكثر من دالة تجميع بحرية:
ما لا يمكنك فعله هو خلط أعمدة غير مُجمَّعة مع دوال التجميع وتتوقّع نتائج منطقية:
-- مسموح به في SQLite، لكن قيمة `customer` تكون عشوائية.
SELECT customer, SUM(amount) FROM orders;
لن يُصدر SQLite خطأً هنا (على عكس قواعد بيانات أخرى)، لكنه سيختار اسم عميل عشوائي ليعرضه بجانب الإجمالي. إذا أردت مجموعًا لكل عميل، فأنت بحاجة إلى GROUP BY — وهذا موضوع الصفحة التالية.
التالي: GROUP BY و HAVING
التجميع على الجدول كله يجيب عن سؤال "كم المجموع الكلي؟"، أما التجميع لكل مجموعة — لكل عميل، أو لكل شهر، أو لكل حالة — فهو الذي يجيب عن الأسئلة الأكثر فائدة. GROUP BY هو الأداة التي تقسّم بها الصفوف إلى مجموعات قبل تطبيق دوال التجميع، و HAVING هو ما يتيح لك تصفية النتائج بعد التجميع. هذا ما سنتناوله بعد قليل.
الأسئلة الشائعة
ما هي دوال التجميع (Aggregate Functions) في SQLite؟
هي دوال تأخذ مجموعة من الصفوف وتُرجع قيمة واحدة تلخّصها. الدوال المدمجة في SQLite هي COUNT وSUM وAVG وMIN وMAX وTOTAL وGROUP_CONCAT. وعند استخدامها بدون GROUP BY، فإنها تختزل نتيجة الاستعلام بأكملها في صف واحد فقط.
ما الفرق بين SUM وTOTAL في SQLite؟
كلاهما يجمع الأرقام، لكن SUM يُرجع NULL عندما تكون كل المدخلات NULL، ويستخدم الحساب الصحيح (integer) عند الإمكان مما قد يُسبّب overflow. أما TOTAL فيُرجع دائمًا قيمة عشرية (floating-point)، ويُرجع 0.0 إذا لم تكن هناك صفوف. استخدم TOTAL إذا أردت ضمان الحصول على ناتج رقمي، وSUM إذا كنت تتبع السلوك القياسي لـ SQL.
كيف أعدّ القيم الفريدة (Distinct) في SQLite؟
ضع DISTINCT داخل الدالة هكذا: COUNT(DISTINCT customer_id). هذا يحسب القيم الفريدة غير الـ NULL فقط. أما COUNT(column) فيعد القيم غير الـ NULL بما فيها المكررة، وCOUNT(*) يعد كل الصفوف بصرف النظر عن وجود NULL أو عدمه.
هل دوال التجميع في SQLite تتجاهل NULL؟
نعم، جميع دوال التجميع تتجاهل قيم NULL ما عدا COUNT(*). مثلًا AVG يقسم على عدد القيم غير الـ NULL وليس على إجمالي الصفوف. الاستثناء الوحيد هو COUNT(*) لأنه يعد الصفوف لا القيم، فيشمل أي صف حتى لو كانت كل أعمدته NULL.