التجميع
يُعد التجميع (aggregation) من القدرات التي تجعلك تقدّر حقًا قوة نظم قواعد البيانات العلائقية. فهو يتيح لك أن تتجاوز مجرد حفظ بياناتك إلى عالم طرح أسئلة مثيرة للاهتمام فعلًا يمكن استخدامها في اتخاذ القرارات. ويتناول هذا القسم التجميع بإسهاب، مستخدمًا التجميع المعياري (grouping) وكذلك دوال النافذة (window functions) الأحدث.
إن واجهت صعوبة في هذه الأسئلة، فأنصح بشدة بكتاب Learning SQL لـ Alan Beaulieu وSQL Cookbook لـ Anthony Molinaro. بل احصل على الأخير على أي حال — فسيأخذك إلى ما هو أبعد من أي شيء تجده في هذا الموقع، وعلى أنظمة قواعد بيانات مختلفة متعددة أيضًا.
1. عدّ عدد المرافق
السؤال
في أول جولة لنا في التجميعات، سنكتفي بشيء بسيط. نريد معرفة عدد المرافق الموجودة — أنتج ببساطة عددًا إجماليًا.
النتائج المتوقعة
| count |
|---|
| 9 |
الإجابة
select count(*) from cd.facilities;
يبدأ التجميع ببساطة إلى حد كبير! فـ SQL أعلاه يختار كل شيء من جدول facilities، ثم يعُدّ عدد الصفوف في مجموعة النتائج. ولدالة count استخدامات متنوعة: COUNT(*) تُعيد ببساطة عدد الصفوف، وCOUNT(address) تعُدّ عدد العناوين غير الفارغة (non-null) في مجموعة النتائج. وأخيرًا، COUNT(DISTINCT address) تعُدّ عدد العناوين المختلفة في جدول facilities.
الفكرة الأساسية للدالة التجميعية (aggregate function) أنها تأخذ عمودًا من البيانات، وتجري عليه عملية ما، وتُخرج قيمة قياسية (واحدة). وهناك مجموعة أخرى كثيرة من دوال التجميع، منها MAX وMIN وSUM وAVG. وكلها تفعل إلى حد كبير ما تتوقعه من أسمائها :-).
ومن جوانب الدوال التجميعية التي يجدها الناس غالبًا مربكةً الاستعلامات مثل ما يلي:
select facid, count(*) from cd.facilities
جرّبه، وستجد أنه لا يعمل. والسبب أن count(*) تريد طيّ جدول facilities في قيمة واحدة — لكنها للأسف لا تستطيع ذلك، لأن في cd.facilities كثيرًا من قيم facid المختلفة — ولا يعرف Postgres أي facid ينبغي أن يقترن به العدد.
وبدلًا من ذلك، إن أردت استعلامًا يُعيد كل قيم facid مع عدد في كل صف، يمكنك إخراج التجميع إلى استعلام فرعي كما يلي:
select facid,
(select count(*) from cd.facilities)
from cd.facilities
وعندما يكون لدينا استعلام فرعي يُعيد قيمة قياسية كهذه، يعرف Postgres أن يكرّر القيمة ببساطة لكل صف في cd.facilities.
تلميح: جرّب البحث في دالة COUNT في SQL
2. عدّ عدد المرافق المكلفة
السؤال
أنتج عدد المرافق التي تكون تكلفتها للضيوف 10 أو أكثر.
النتائج المتوقعة
| count |
|---|
| 6 |
الإجابة
select count(*) from cd.facilities where guestcost >= 10;
هذا السؤال مجرد تعديل بسيط على السؤال السابق: نحتاج إلى استبعاد المرافق غير المكلفة. ويسهل فعل ذلك بعبارة WHERE. فلم يبقَ تجميعنا يرى سوى المرافق المكلفة.
تلميح: ستحتاج إلى إضافة عبارة WHERE إلى إجابة السؤال السابق.
3. عدّ عدد التوصيات التي يقدّمها كل عضو
السؤال
أنتج عدد التوصيات التي قدّمها كل عضو. ورتّب بحسب معرّف العضو.
النتائج المتوقعة
| recommendedby | count |
|---|---|
| 1 | 5 |
| 2 | 3 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 1 |
| 9 | 2 |
| 11 | 1 |
| 13 | 2 |
| 15 | 1 |
| 16 | 1 |
| 20 | 1 |
| 30 | 1 |
الإجابة
select recommendedby, count(*)
from cd.members
where recommendedby is not null
group by recommendedby
order by recommendedby;
رأينا سابقًا أن دوال التجميع تُطبَّق على عمود من القيم، وتحوّله إلى قيمة قياسية مجمّعة. وهذا مفيد، لكننا كثيرًا ما نجد أننا لا نريد نتيجة مجمّعة واحدة فقط: فمثلًا، بدلًا من معرفة إجمالي المال الذي جناه النادي هذا الشهر، قد أريد معرفة مقدار المال الذي جناه كل مرفق، أو أي أوقات اليوم كانت الأكثر ربحًا.
لدعم هذا النوع من السلوك، توفّر SQL تركيبة GROUP BY. وهي تجمع البيانات في مجموعات، وتشغّل دالة التجميع بصورة منفصلة لكل مجموعة. وعندما تحدد GROUP BY، تنتج قاعدة البيانات قيمة مجمّعة لكل قيمة مميزة في الأعمدة المقدّمة. وفي هذه الحالة، نقول: 'لكل قيمة مميزة من recommendedby، أعطني عدد مرات ظهور تلك القيمة'.
تلميح: جرّب هذه المرة البحث في GROUP BY مع count. ولا تنسَ تصفية الموصين الفارغين!
4. عرض إجمالي الشرائح المحجوزة لكل مرفق
السؤال
أنتج قائمة بالعدد الإجمالي للشرائح المحجوزة لكل مرفق. ويكفي في الوقت الحالي إنتاج جدول إخراج يتكوّن من معرّف المرفق والشرائح، مرتّبًا بحسب معرّف المرفق.
النتائج المتوقعة
| facid | Total Slots |
|---|---|
| 0 | 1320 |
| 1 | 1278 |
| 2 | 1209 |
| 3 | 830 |
| 4 | 1404 |
| 5 | 228 |
| 6 | 1104 |
| 7 | 908 |
| 8 | 911 |
الإجابة
select facid, sum(slots) as "Total Slots"
from cd.bookings
group by facid
order by facid;
إلى جانب تقديمنا الدالة التجميعية SUM، لا يوجد الكثير مما يُقال عن هذا التمرين. فلكل معرّف مرفق مميز، تجمع دالة SUM كل القيم في عمود slots.
تلميح: في هذا السؤال ستحتاج إلى الاطلاع على الدالة التجميعية SUM.
5. عرض إجمالي الشرائح المحجوزة لكل مرفق في شهر معيّن
السؤال
أنتج قائمة بالعدد الإجمالي للشرائح المحجوزة لكل مرفق في شهر سبتمبر 2012. وأنتج جدول إخراج يتكوّن من معرّف المرفق والشرائح، مرتّبًا بحسب عدد الشرائح.
النتائج المتوقعة
| facid | Total Slots |
|---|---|
| 5 | 122 |
| 3 | 422 |
| 7 | 426 |
| 8 | 471 |
| 6 | 540 |
| 2 | 570 |
| 1 | 588 |
| 0 | 591 |
| 4 | 648 |
الإجابة
select facid, sum(slots) as "Total Slots"
from cd.bookings
where
starttime >= '2012-09-01'
and starttime < '2012-10-01'
group by facid
order by sum(slots);
هذا تعديل طفيف على مثالنا السابق. وتذكّر أن التجميع يحدث بعد تقييم عبارة WHERE: لذا نستخدم WHERE لتقييد البيانات التي نجمّعها، فلا يرى تجميعنا إلا بيانات شهر واحد.
تلميح: يمكنك تقييد البيانات التي تدخل في دوالك التجميعية باستخدام عبارة WHERE.
6. عرض إجمالي الشرائح المحجوزة لكل مرفق في كل شهر
السؤال
أنتج قائمة بالعدد الإجمالي للشرائح المحجوزة لكل مرفق في كل شهر من سنة 2012. وأنتج جدول إخراج يتكوّن من معرّف المرفق والشرائح، مرتّبًا بحسب المعرّف والشهر.
النتائج المتوقعة
| facid | month | Total Slots |
|---|---|---|
| 0 | 7 | 270 |
| 0 | 8 | 459 |
| 0 | 9 | 591 |
| 1 | 7 | 207 |
| 1 | 8 | 483 |
| 1 | 9 | 588 |
| 2 | 7 | 180 |
| 2 | 8 | 459 |
| 2 | 9 | 570 |
| 3 | 7 | 104 |
| 3 | 8 | 304 |
| 3 | 9 | 422 |
| 4 | 7 | 264 |
| 4 | 8 | 492 |
| 4 | 9 | 648 |
| 5 | 7 | 24 |
| 5 | 8 | 82 |
| 5 | 9 | 122 |
| 6 | 7 | 164 |
| 6 | 8 | 400 |
| 6 | 9 | 540 |
| 7 | 7 | 156 |
| 7 | 8 | 326 |
| 7 | 9 | 426 |
| 8 | 7 | 117 |
| 8 | 8 | 322 |
| 8 | 9 | 471 |
الإجابة
select facid, extract(month from starttime) as month, sum(slots) as "Total Slots"
from cd.bookings
where extract(year from starttime) = 2012
group by facid, month
order by facid, month;
الجزء الأساسي الجديد في هذا السؤال هو دالة EXTRACT. فهي تتيح لك الحصول على مكونات فردية من طابع زمني، مثل اليوم والشهر والسنة، إلخ. ونجمّع بحسب ناتج هذه الدالة لتوفير قيم لكل شهر. وبديل ذلك، إن احتجنا إلى التمييز بين الشهر نفسه في سنوات مختلفة، استخدام دالة DATE_TRUNC التي تقتطع تاريخًا عند درجة تفصيل معيّنة. ومن الجدير بالذكر أيضًا أن هذه أول مرة نستخدم فيها حقًا القدرة على التجميع بحسب أكثر من عمود واحد.
ومن الأمور التي يجدر مراعاتها في هذه الإجابة أن استخدام دالة EXTRACT في عبارة WHERE قد يسبب مشكلات أداء حادة في الجداول الكبيرة. فإذا كان على عمود الطابع الزمني فهرس عادي، فلن يفهم Postgres أنه يستطيع استخدام الفهرس لتسريع الاستعلام، وسيضطر بدلًا من ذلك إلى مسح الجدول كله. ولديك خياران هنا:
فكّر في إنشاء فهرس قائم على تعبير على عمود الطابع الزمني. فمع فهارس محددة على نحو مناسب، يستطيع Postgres استخدام الفهارس لتسريع عبارات WHERE التي تحتوي نداءات دوال. أو عدّل الاستعلام ليكون أكثر إسهابًا قليلًا، لكن باستخدام مقارنات أكثر معيارية، على سبيل المثال:
select facid, extract(month from starttime) as month, sum(slots) as "Total Slots"
from cd.bookings
where
starttime >= '2012-01-01'
and starttime < '2013-01-01'
group by facid, month
order by facid, month;
يستطيع Postgres استخدام فهرس مع هذه المقارنات المعيارية دون أي مساعدة إضافية.
تلميح: ألقِ نظرة على دالة EXTRACT.
7. إيجاد عدد الأعضاء الذين أجروا حجزًا واحدًا على الأقل
السؤال
أوجد العدد الإجمالي للأعضاء (بمن فيهم الضيوف) الذين أجروا حجزًا واحدًا على الأقل.
النتائج المتوقعة
| count |
|---|
| 30 |
الإجابة
select count(distinct memid) from cd.bookings
قد يكون حدسك الأول استخدام استعلام فرعي هنا. على شيء مثل ما يلي:
select count(*) from
(select distinct memid from cd.bookings) as mems
وهذا يعمل على أحسن وجه، لكن يمكننا تبسيطه قليلًا بمساعدة معرفة إضافية صغيرة في صورة COUNT DISTINCT. وهي تفعل ما قد تتوقعه، إذ تعُدّ القيم المميزة في العمود الممرَّر.
تلميح: ألقِ نظرة على COUNT DISTINCT
8. عرض المرافق التي حُجز فيها أكثر من 1000 شريحة
السؤال
أنتج قائمة بالمرافق التي حُجز فيها أكثر من 1000 شريحة. وأنتج جدول إخراج يتكوّن من معرّف المرفق والشرائح، مرتّبًا بحسب معرّف المرفق.
النتائج المتوقعة
| facid | Total Slots |
|---|---|
| 0 | 1320 |
| 1 | 1278 |
| 2 | 1209 |
| 4 | 1404 |
| 6 | 1104 |
الإجابة
select facid, sum(slots) as "Total Slots"
from cd.bookings
group by facid
having sum(slots) > 1000
order by facid
يتبيّن أن هناك في الواقع كلمة مفتاحية في SQL مصمّمة للمساعدة في تصفية ناتج دوال التجميع. وهذه الكلمة هي HAVING.
يسهل الخلط بين سلوك HAVING وسلوك WHERE. وأفضل طريقة للتفكير في الأمر أنه في سياق استعلام فيه دالة تجميعية، تُستخدم WHERE لتصفية البيانات التي تدخل إلى الدالة التجميعية، بينما تُستخدم HAVING لتصفية البيانات بعد خروجها من الدالة. جرّب أن تجرّب لتستكشف هذا الفرق!
تلميح: جرّب البحث في عبارة HAVING.
9. إيجاد الإيراد الإجمالي لكل مرفق
السؤال
أنتج قائمة بالمرافق مع إيرادها الإجمالي. وينبغي أن يتكوّن جدول الإخراج من اسم المرفق والإيراد، مرتّبًا بحسب الإيراد. وتذكّر أن التكلفة مختلفة للضيوف والأعضاء!
النتائج المتوقعة
| name | revenue |
|---|---|
| Table Tennis | 180 |
| Snooker Table | 240 |
| Pool Table | 270 |
| Badminton Court | 1906.5 |
| Squash Court | 13468.0 |
| Tennis Court 1 | 13860 |
| Tennis Court 2 | 14310 |
| Massage Room 2 | 15810 |
| Massage Room 1 | 72540 |
الإجابة
select facs.name, sum(slots * case
when memid = 0 then facs.guestcost
else facs.membercost
end) as revenue
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.name
order by revenue;
التعقيد الحقيقي الوحيد في هذا الاستعلام أن الضيوف (معرّف العضو 0) لهم تكلفة مختلفة عن الجميع. فنستخدم عبارة case لإنتاج تكلفة كل جلسة، ثم نجمع تلك الجلسات، مجمّعة بحسب المرفق.
تلميح: تذكّر عبارة CASE!
10. إيجاد المرافق التي يقل إيرادها الإجمالي عن 1000
السؤال
أنتج قائمة بالمرافق التي يقل إيرادها الإجمالي عن 1000. وأنتج جدول إخراج يتكوّن من اسم المرفق والإيراد، مرتّبًا بحسب الإيراد. وتذكّر أن التكلفة مختلفة للضيوف والأعضاء!
النتائج المتوقعة
| name | revenue |
|---|---|
| Table Tennis | 180 |
| Snooker Table | 240 |
| Pool Table | 270 |
الإجابة
select name, revenue from (
select facs.name, sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) as revenue
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.name
) as agg where revenue < 1000
order by revenue;
ربما حاولت استخدام الكلمة المفتاحية HAVING التي قدّمناها في تمرين سابق، فأنتجت شيئًا مثل ما يلي:
select facs.name, sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) as revenue
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.name
having revenue < 1000
order by revenue;
للأسف، هذا لا يعمل! وستحصل على خطأ من قبيل ERROR: column "revenue" does not exist. فـ Postgres، بخلاف بعض نظم إدارة قواعد البيانات العلائقية الأخرى مثل SQL Server وMySQL، لا يدعم وضع أسماء الأعمدة في عبارة HAVING. وهذا يعني أنه لكي يعمل هذا الاستعلام، سيتعيّن عليك إنتاج شيء مثل ما يلي:
select facs.name, sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) as revenue
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.name
having sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) < 1000
order by revenue;
تكرار شيفرة حسابية كبيرة كهذه أمر غير مرتب، لذا يكتفي حلّنا المعتمد بلفّ جسم الاستعلام الرئيسي كاستعلام فرعي، والاختيار منه بعبارة WHERE. وبوجه عام، أنصح باستخدام HAVING في الاستعلامات البسيطة، لأنه يزيد الوضوح. وإلا فإن مقاربة الاستعلام الفرعي هذه غالبًا أسهل استخدامًا.
تلميح: قد تجد HAVING صعبة الاستخدام هنا. جرّب استعلامًا فرعيًا بدلًا منها. وستحتاج على الأرجح إلى عبارة CASE أيضًا.
11. إخراج معرّف المرفق الذي حُجز فيه أكبر عدد من الشرائح
السؤال
أخرج معرّف المرفق الذي حُجز فيه أكبر عدد من الشرائح. ولنقاط إضافية، جرّب نسخة دون عبارة LIMIT. وستبدو هذه النسخة غير مرتبة على الأرجح!
النتائج المتوقعة
| facid | Total Slots |
|---|---|
| 4 | 1404 |
الإجابة
select facid, sum(slots) as "Total Slots"
from cd.bookings
group by facid
order by sum(slots) desc
LIMIT 1;
لنبدأ بما يمكن اعتباره أبسط طريقة لفعل ذلك: أنتج قائمة بمعرّفات المرافق وإجمالي عدد الشرائح المستخدمة، ورتّبها بحسب إجمالي عدد الشرائح المستخدمة، واختر النتيجة الأولى فقط.
لكن يجدر إدراك أن لهذه الطريقة ضعفًا كبيرًا. ففي حال التعادل، سنحصل مع ذلك على نتيجة واحدة فقط! وللحصول على كل النتائج ذات الصلة، قد نجرّب استخدام الدالة التجميعية MAX، على شيء مثل ما يلي:
select facid, max(totalslots) from (
select facid, sum(slots) as totalslots
from cd.bookings
group by facid
) as sub group by facid
المقصود من هذا الاستعلام الحصول على أكبر قيمة totalslots ومعرّف المرفق (أو المعرّفات) المرتبط بها. لكن هذا للأسف لن يعمل! ففي حال وجود عدة قيم facid لها عدد الشرائح المحجوزة نفسه، سيصبح من الملتبس أي facid ينبغي إقرانه بالقيمة الواحدة (أو القياسية) الخارجة من دالة MAX. وهذا يعني أن Postgres سيخبرك بأن facid ينبغي أن يكون في قسم GROUP BY، وهو ما لن ينتج النتائج التي نبحث عنها.
ولنحاول محاولة أولى في استعلام يعمل:
select facid, sum(slots) as totalslots
from cd.bookings
group by facid
having sum(slots) = (select max(sum2.totalslots) from
(select sum(slots) as totalslots
from cd.bookings
group by facid
) as sum2);
ينتج الاستعلام قائمة بمعرّفات المرافق وعدد الشرائح المستخدمة، ثم يستخدم عبارة HAVING تحسب أكبر قيمة totalslots. ونحن نقول في جوهر الأمر: 'أنتج قائمة بقيم facid وعدد الشرائح المحجوزة لكل منها، واستبعد كل ما ليس عدد شرائحه المحجوزة مساويًا للأقصى.'
لكن رغم فائدة HAVING، فإن استعلامنا قبيح إلى حد كبير. ولتحسينه، لنُقدّم مفهومًا جديدًا آخر: التعبيرات الجدولية الشائعة (Common Table Expressions، أو CTEs). ويمكن اعتبار تعبيرات CTE تتيح لك تعريف عرض لقاعدة البيانات ضمن استعلامك. وهي مفيدة حقًا في حالات كهذه، حيث تضطر إلى تكرار نفسك كثيرًا.
وتُعرّف تعبيرات CTE بالصورة WITH CTEName as (SQL-Expression). ويمكنك أن ترى استعلامنا معادًا تعريفه باستخدام تعبير CTE أدناه:
with sum as (select facid, sum(slots) as totalslots
from cd.bookings
group by facid
)
select facid, totalslots
from sum
where totalslots = (select max(totalslots) from sum);
ويمكنك أن ترى أننا أخرجنا اختياراتنا المتكررة من cd.bookings إلى تعبير CTE واحد، وجعلنا الاستعلام أسهل قراءةً كثيرًا في أثناء ذلك!
لكن انتظر. هناك المزيد. فمن الممكن أيضًا حلّ هذه المسألة باستخدام دوال النافذة. وسنؤجلها إلى وقت لاحق، لكن هناك حلول أفضل لمسائل كهذه.
هذه معلومات كثيرة لتمرين واحد. فلا تقلق كثيرًا إن لم تفهمها كلها الآن — سنعيد استخدام هذه المفاهيم في تمارين لاحقة.
تلميح: ضع في اعتبارك استخدام الكلمة المفتاحية LIMIT مع ORDER BY. وفي النسخة الخالية من LIMIT، ستحتاج على الأرجح إلى البحث في الكلمة المفتاحية HAVING. واعلم أن النسخة الأخيرة صعبة!
12. عرض إجمالي الشرائح المحجوزة لكل مرفق في كل شهر — الجزء 2
السؤال
أنتج قائمة بالعدد الإجمالي للشرائح المحجوزة لكل مرفق في كل شهر من سنة 2012. وفي هذه النسخة، أدرج صفوف إخراج تحتوي إجماليات لكل الأشهر لكل مرفق، وإجماليًا لكل الأشهر لكل المرافق. وينبغي أن يتكوّن جدول الإخراج من معرّف المرفق والشهر والشرائح، مرتّبًا بحسب المعرّف والشهر. وعند حساب القيم المجمّعة لكل الأشهر وكل قيم facid، أرجع قيم null في عمودَي month وfacid.
النتائج المتوقعة
| facid | month | slots |
|---|---|---|
| 0 | 7 | 270 |
| 0 | 8 | 459 |
| 0 | 9 | 591 |
| 0 | 1320 | |
| 1 | 7 | 207 |
| 1 | 8 | 483 |
| 1 | 9 | 588 |
| 1 | 1278 | |
| 2 | 7 | 180 |
| 2 | 8 | 459 |
| 2 | 9 | 570 |
| 2 | 1209 | |
| 3 | 7 | 104 |
| 3 | 8 | 304 |
| 3 | 9 | 422 |
| 3 | 830 | |
| 4 | 7 | 264 |
| 4 | 8 | 492 |
| 4 | 9 | 648 |
| 4 | 1404 | |
| 5 | 7 | 24 |
| 5 | 8 | 82 |
| 5 | 9 | 122 |
| 5 | 228 | |
| 6 | 7 | 164 |
| 6 | 8 | 400 |
| 6 | 9 | 540 |
| 6 | 1104 | |
| 7 | 7 | 156 |
| 7 | 8 | 326 |
| 7 | 9 | 426 |
| 7 | 908 | |
| 8 | 7 | 117 |
| 8 | 8 | 322 |
| 8 | 9 | 471 |
| 8 | 910 | |
| 9191 |
الإجابة
select facid, extract(month from starttime) as month, sum(slots) as slots
from cd.bookings
where
starttime >= '2012-01-01'
and starttime < '2013-01-01'
group by rollup(facid, month)
order by facid, month;
عندما نجري تحليل بيانات، نريد أحيانًا تنفيذ مستويات متعددة من التجميع لنتمكن من 'التقريب والتبعيد' إلى أعماق مختلفة. وفي هذه الحالة، قد ننظر إلى الاستخدام الإجمالي لكل مرفق، ثم نريد الغوص لرؤية أدائه على أساس شهري. وباستخدام SQL التي نعرفها حتى الآن، يصبح إنتاج استعلام واحد يفعل ما نريد مرهقًا إلى حد كبير — إذ نضطر فعليًا إلى دمج عدة استعلامات باستخدام UNION ALL:
select facid, extract(month from starttime) as month, sum(slots) as slots
from cd.bookings
where
starttime >= '2012-01-01'
and starttime = '2012-01-01'
and starttime = '2012-01-01'
and starttime < '2013-01-01'
order by facid, month;
وكما ترى، ينفّذ كل استعلام فرعي مستوى مختلفًا من التجميع، ونحن نجمع النتائج فقط. ويمكننا تنظيف ذلك كثيرًا بإخراج القواسم المشتركة إلى تعبير CTE:
with bookings as (
select facid, extract(month from starttime) as month, slots
from cd.bookings
where
starttime >= '2012-01-01'
and starttime < '2013-01-01'
)
select facid, month, sum(slots) from bookings group by facid, month
union all
select facid, null, sum(slots) from bookings group by facid
union all
select null, null, sum(slots) from bookings
order by facid, month;
هذه النسخة ليست مؤذية للنظر أكثر من اللازم، لكنها تصبح مرهقة مع تزايد عدد أعمدة التجميع. ولحسن الحظ، أدخل PostgreSQL 9.5 دعمًا لمُعامل ROLLUP، وقد استخدمناه لتبسيط إجابتنا المعتمدة.
ينتج ROLLUP تسلسلًا هرميًا من التجميعات بالترتيب الممرَّر إليه: فمثلًا، يُخرج ROLLUP(facid, month) تجميعات على (facid, month) و(facid) و(). ولو أردنا تجميعًا لكل المرافق لشهر معيّن (بدلًا من كل الأشهر لمرفق معيّن)، لوجب علينا عكس الترتيب باستخدام ROLLUP(month, facid). وبديلًا، إن أردنا كل التبديلات الممكنة للأعمدة التي نمرّرها، يمكننا استخدام CUBE بدلًا من ROLLUP. وسينتج ذلك (facid, month) و(month) و(facid) و().
وROLLUP وCUBE حالتان خاصتان من GROUPING SETS. وتتيح لك GROUPING SETS تحديد تبديلات التجميع التي تريدها بالضبط: فيمكنك، مثلًا، طلب (facid, month) و(facid) فقط، مع تخطّي التجميع في المستوى الأعلى.
تلميح: ابحث عن مُعامل ROLLUP في Postgres.
13. عرض إجمالي الساعات المحجوزة لكل مرفق مسمّى
السؤال
أنتج قائمة بالعدد الإجمالي للساعات المحجوزة لكل مرفق، مع تذكّر أن الشريحة الواحدة مدتها نصف ساعة. وينبغي أن يتكوّن جدول الإخراج من معرّف المرفق واسمه والساعات المحجوزة، مرتّبًا بحسب معرّف المرفق. وحاول تنسيق الساعات إلى منزلتين عشريتين.
النتائج المتوقعة
| facid | name | Total Hours |
|---|---|---|
| 0 | Tennis Court 1 | 660.00 |
| 1 | Tennis Court 2 | 639.00 |
| 2 | Badminton Court | 604.50 |
| 3 | Table Tennis | 415.00 |
| 4 | Massage Room 1 | 702.00 |
| 5 | Massage Room 2 | 114.00 |
| 6 | Squash Court | 552.00 |
| 7 | Snooker Table | 454.00 |
| 8 | Pool Table | 455.50 |
الإجابة
select facs.facid, facs.name,
trim(to_char(sum(bks.slots)/2.0, '9999999999999999D99')) as "Total Hours"
from cd.bookings bks
inner join cd.facilities facs
on facs.facid = bks.facid
group by facs.facid, facs.name
order by facs.facid;
هناك بضعة أمور صغيرة مثيرة للاهتمام في هذا السؤال. أولًا، ترى أن تجميعنا يعمل على أحسن وجه حين نربط جدولًا آخر بعلاقة 1:1. ولاحظ أيضًا أننا نجمّع بحسب facs.facid وfacs.name معًا. وقد يبدو هذا غريبًا: فبعد كل شيء، بما أن facid هو المفتاح الأساسي لجدول facilities، فلكل facid اسم واحد بالضبط، والتجميع بحسب الحقلين كليهما مماثل للتجميع بحسب facid وحده. بل إنك ستجد أن الاستعلام يعمل على أحسن وجه إن أزلت facs.name من عبارة GROUP BY: إذ يستنتج Postgres وجود هذه العلاقة 1:1، ولا يصرّ على أن نجمّع بحسب العمودين كليهما.
لكن للأسف، بحسب نظام قواعد البيانات الذي نستخدمه، قد لا يكون التحقق بهذا الذكاء، وقد لا يدرك أن العلاقة 1:1 قطعًا. وفي هذه الحالة، لو كانت هناك أسماء متعددة لكل facid ولم نكن جمّعنا بحسب الاسم، لاضطر نظام إدارة قواعد البيانات إلى الاختيار بين عدة خيارات (متساوية الصحة) للاسم. ولأن هذا غير صالح، سيصرّ نظام قاعدة البيانات على أن نجمّع بحسب الحقلين كليهما. وبوجه عام، أنصح بالتجميع بحسب كل الأعمدة التي ليست داخل دالة تجميعية: فهذا يضمن توافقية أفضل بين المنصات.
ثم تأتي القسمة. وقد يعرف من بينكم من هم على دراية بـ MySQL أن القسمات الصحيحة تُحوَّل تلقائيًا إلى أعداد عشرية. أما Postgres فأكثر تقليدية قليلًا في هذا الشأن، ويتوقع منك أن تخبره إن أردت قسمة عشرية. ويمكنك فعل ذلك بسهولة هنا بالقسمة على 2.0 بدلًا من 2.
وأخيرًا، لننظر إلى التنسيق. تحوّل دالة TO_CHAR القيم إلى سلاسل محارف. وهي تأخذ نص تنسيق نحدده كـ(عدد كبير من الأرقام) قبل العلامة العشرية، فالعلامة العشرية، فرقمين بعدها. وقد يُضاف فراغ في مقدمة ناتج هذه الدالة، ولهذا نضمّ دالة TRIM الخارجية.
تلميح: تذكّر أنه في Postgres تؤدي قسمة عددين صحيحين إلى قسمة صحيحة. وأنت تريد هنا قسمة عشرية. ولتنسيق الساعات، ألقِ نظرة على دالة to_char، مع تذكّر إزالة أي مسافات بيضاء متبقية
14. عرض أول حجز لكل عضو بعد 1 سبتمبر 2012
السؤال
أنتج قائمة باسم كل عضو ومعرّفه وأول حجز له بعد 1 سبتمبر 2012. ورتّب بحسب معرّف العضو.
النتائج المتوقعة
| surname | firstname | memid | starttime |
|---|---|---|---|
| GUEST | GUEST | 0 | 2012-09-01 08:00:00 |
| Smith | Darren | 1 | 2012-09-01 09:00:00 |
| Smith | Tracy | 2 | 2012-09-01 11:30:00 |
| Rownam | Tim | 3 | 2012-09-01 16:00:00 |
| Joplette | Janice | 4 | 2012-09-01 15:00:00 |
| Butters | Gerald | 5 | 2012-09-02 12:30:00 |
| Tracy | Burton | 6 | 2012-09-01 15:00:00 |
| Dare | Nancy | 7 | 2012-09-01 12:30:00 |
| Boothe | Tim | 8 | 2012-09-01 08:30:00 |
| Stibbons | Ponder | 9 | 2012-09-01 11:00:00 |
| Owen | Charles | 10 | 2012-09-01 11:00:00 |
| Jones | David | 11 | 2012-09-01 09:30:00 |
| Baker | Anne | 12 | 2012-09-01 14:30:00 |
| Farrell | Jemima | 13 | 2012-09-01 09:30:00 |
| Smith | Jack | 14 | 2012-09-01 11:00:00 |
| Bader | Florence | 15 | 2012-09-01 10:30:00 |
| Baker | Timothy | 16 | 2012-09-01 15:00:00 |
| Pinker | David | 17 | 2012-09-01 08:30:00 |
| Genting | Matthew | 20 | 2012-09-01 18:00:00 |
| Mackenzie | Anna | 21 | 2012-09-01 08:30:00 |
| Coplin | Joan | 22 | 2012-09-02 11:30:00 |
| Sarwin | Ramnaresh | 24 | 2012-09-04 11:00:00 |
| Jones | Douglas | 26 | 2012-09-08 13:00:00 |
| Rumney | Henrietta | 27 | 2012-09-16 13:30:00 |
| Farrell | David | 28 | 2012-09-18 09:00:00 |
| Worthington-Smyth | Henry | 29 | 2012-09-19 09:30:00 |
| Purview | Millicent | 30 | 2012-09-19 11:30:00 |
| Tupperware | Hyacinth | 33 | 2012-09-20 08:00:00 |
| Hunt | John | 35 | 2012-09-23 14:00:00 |
| Crumpet | Erica | 36 | 2012-09-27 11:30:00 |
الإجابة
select mems.surname, mems.firstname, mems.memid, min(bks.starttime) as starttime
from cd.bookings bks
inner join cd.members mems on
mems.memid = bks.memid
where starttime >= '2012-09-01'
group by mems.surname, mems.firstname, mems.memid
order by mems.memid;
توضح هذه الإجابة استخدام دوال التجميع على التواريخ. وتعمل MIN تمامًا كما تتوقع، إذ تستخرج أقل تاريخ ممكن في مجموعة النتائج. ولكي يعمل ذلك، نحتاج إلى ضمان ألا تحتوي مجموعة النتائج إلا تواريخ من سبتمبر فصاعدًا. ونفعل ذلك بعبارة WHERE.
وقد تستخدم استعلامًا كهذا عادةً لإيجاد الحجز التالي لأحد العملاء. ويمكنك استخدامه باستبدال التاريخ '2012-09-01' بالدالة now()
تلميح: ألقِ نظرة على الدالة التجميعية MIN
15. إنتاج قائمة بأسماء الأعضاء، بحيث يحتوي كل صف على العدد الإجمالي للأعضاء
السؤال
أنتج قائمة بأسماء الأعضاء، بحيث يحتوي كل صف على العدد الإجمالي للأعضاء. ورتّب بحسب تاريخ الانضمام، وأدرج الأعضاء الضيوف.
النتائج المتوقعة
| count | firstname | surname |
|---|---|---|
| 31 | GUEST | GUEST |
| 31 | Darren | Smith |
| 31 | Tracy | Smith |
| 31 | Tim | Rownam |
| 31 | Janice | Joplette |
| 31 | Gerald | Butters |
| 31 | Burton | Tracy |
| 31 | Nancy | Dare |
| 31 | Tim | Boothe |
| 31 | Ponder | Stibbons |
| 31 | Charles | Owen |
| 31 | David | Jones |
| 31 | Anne | Baker |
| 31 | Jemima | Farrell |
| 31 | Jack | Smith |
| 31 | Florence | Bader |
| 31 | Timothy | Baker |
| 31 | David | Pinker |
| 31 | Matthew | Genting |
| 31 | Anna | Mackenzie |
| 31 | Joan | Coplin |
| 31 | Ramnaresh | Sarwin |
| 31 | Douglas | Jones |
| 31 | Henrietta | Rumney |
| 31 | David | Farrell |
| 31 | Henry | Worthington-Smyth |
| 31 | Millicent | Purview |
| 31 | Hyacinth | Tupperware |
| 31 | John | Hunt |
| 31 | Erica | Crumpet |
| 31 | Darren | Smith |
الإجابة
select count(*) over(), firstname, surname
from cd.members
order by joindate
باستخدام المعرفة التي بنيناها حتى الآن، تكون الإجابة الأكثر بداهة كما يلي. ونستخدم استعلامًا فرعيًا لأن SQL ستطلب منا خلاف ذلك التجميع بحسب firstname وsurname، ما ينتج نتيجة مختلفة عمّا نبحث عنه.
select (select count(*) from cd.members) as count, firstname, surname
from cd.members
order by joindate
لا شيء على الإطلاق خطأ في هذه الإجابة، لكننا اخترنا مقاربة مختلفة لتقديم مفهوم جديد يُسمى دوال النافذة. وتوفّر دوال النافذة قدرات هائلة القوة، بصورة غالبًا ما تكون أكثر ملاءمة من دوال التجميع المعيارية. ورغم أن هذا التمرين مجرد تمرين بسيط، فسنعمل على أمثلة أكثر تعقيدًا في المستقبل القريب.
تعمل دوال النافذة على مجموعة نتائج استعلامك (أو استعلامك الفرعي)، بعد عبارة WHERE وكل التجميع المعياري. وهي تعمل على نافذة من البيانات. وهذه النافذة افتراضيًا غير مقيّدة: أي مجموعة النتائج كلها، لكن يمكن تقييدها لتقديم نتائج أكثر فائدة. فمثلًا، لنفترض أننا نريد، بدلًا من عدد كل الأعضاء، عدد كل الأعضاء الذين انضموا في الشهر نفسه الذي انضم فيه ذلك العضو:
select count(*) over(partition by date_trunc('month',joindate)),
firstname, surname
from cd.members
order by joindate
في هذا المثال، نقسّم البيانات بحسب الشهر. ولكل صف تعمل عليه دالة النافذة، تكون النافذة كل الصفوف التي لها joindate في الشهر نفسه. وهكذا تنتج دالة النافذة عدد الأعضاء الذين انضموا في ذلك الشهر.
ويمكنك أن تذهب أبعد من ذلك. تخيّل أنك تريد، بدلًا من العدد الإجمالي للأعضاء الذين انضموا ذلك الشهر، معرفة ترتيب العضو بين المنضمين في ذلك الشهر. ويمكنك فعل ذلك بإضافة ORDER BY إلى دالة النافذة:
select count(*) over(partition by date_trunc('month',joindate) order by joindate),
firstname, surname
from cd.members
order by joindate
وتغيّر ORDER BY النافذة مرة أخرى. فبدلًا من أن تكون نافذة كل صف هي القسم كله، تمتد النافذة من بداية القسم إلى الصف الحالي، دون تجاوزه. وهكذا، بالنسبة لأول عضو ينضم في شهر معيّن، يكون العدد 1. وللثاني يكون 2، وهكذا.
وأمر أخير يجدر ذكره عن دوال النافذة: يمكن أن يكون لديك عدة دوال نافذة غير مترابطة في الاستعلام نفسه. وجرّب الاستعلام أدناه كمثال — سترى الأرقام الخاصة بالأعضاء تسير في اتجاهين متعاكسين! وهذه المرونة قد تؤدي إلى استعلامات أكثر إيجازًا ووضوحًا وسهولة في الصيانة.
select count(*) over(partition by date_trunc('month',joindate) order by joindate asc),
count(*) over(partition by date_trunc('month',joindate) order by joindate desc),
firstname, surname
from cd.members
order by joindate
دوال النافذة قوية بشكل استثنائي، وستغيّر طريقة كتابتك لـ SQL وتفكيرك فيها. أحسن استخدامها!
تلميح: اقرأ عن دالة النافذة COUNT.
16. إنتاج قائمة مرقّمة بالأعضاء
السؤال
أنتج قائمة مرقّمة متزايدة باطراد بالأعضاء (بمن فيهم الضيوف)، مرتّبة بحسب تاريخ انضمامهم. وتذكّر أن معرّفات الأعضاء لا يُضمن تسلسلها.
النتائج المتوقعة
| row_number | firstname | surname |
|---|---|---|
| 1 | GUEST | GUEST |
| 2 | Darren | Smith |
| 3 | Tracy | Smith |
| 4 | Tim | Rownam |
| 5 | Janice | Joplette |
| 6 | Gerald | Butters |
| 7 | Burton | Tracy |
| 8 | Nancy | Dare |
| 9 | Tim | Boothe |
| 10 | Ponder | Stibbons |
| 11 | Charles | Owen |
| 12 | David | Jones |
| 13 | Anne | Baker |
| 14 | Jemima | Farrell |
| 15 | Jack | Smith |
| 16 | Florence | Bader |
| 17 | Timothy | Baker |
| 18 | David | Pinker |
| 19 | Matthew | Genting |
| 20 | Anna | Mackenzie |
| 21 | Joan | Coplin |
| 22 | Ramnaresh | Sarwin |
| 23 | Douglas | Jones |
| 24 | Henrietta | Rumney |
| 25 | David | Farrell |
| 26 | Henry | Worthington-Smyth |
| 27 | Millicent | Purview |
| 28 | Hyacinth | Tupperware |
| 29 | John | Hunt |
| 30 | Erica | Crumpet |
| 31 | Darren | Smith |
الإجابة
select row_number() over(order by joindate), firstname, surname
from cd.members
order by joindate
هذا التمرين تدريب بسيط على دوال النافذة! ويمكنك بسهولة مماثلة استخدام count(*) over(order by joindate) هنا، فلا تقلق إن استخدمت ذلك بدلًا منه.
في هذا الاستعلام لا نعرّف قسمًا، ما يعني أن القسم هو مجموعة البيانات كلها. ولأننا نعرّف ترتيبًا لدالة النافذة، فإن النافذة لأي صف معطى هي: من بداية مجموعة البيانات -> الصف الحالي.
تلميح: اقرأ عن دالة النافذة ROW_NUMBER.
17. إخراج معرّف المرفق الذي حُجز فيه أكبر عدد من الشرائح، مرة أخرى
السؤال
أخرج معرّف المرفق الذي حُجز فيه أكبر عدد من الشرائح. وتأكد في حال التعادل من إخراج كل النتائج المتعادلة.
النتائج المتوقعة
| facid | total |
|---|---|
| 4 | 1404 |
الإجابة
select facid, total from (
select facid, sum(slots) total, rank() over (order by sum(slots) desc) rank
from cd.bookings
group by facid
) as ranked
where rank = 1
قد تتذكر أن هذه مسألة حللناها بالفعل في تمرين سابق. وقد توصّلنا إلى إجابة مثل ما يلي، ثم اختصرناها باستخدام تعبيرات CTE:
select facid, sum(slots) as totalslots
from cd.bookings
group by facid
having sum(slots) = (select max(sum2.totalslots) from
(select sum(slots) as totalslots
from cd.bookings
group by facid
) as sum2);
وبعد أن نظّفناها، يصبح هذا الحل ملائمًا تمامًا. لكن شرح كيفية عمل الاستعلام يجعله يبدو غريبًا قليلًا — 'أوجد عدد الشرائح التي حجزها أفضل مرفق. احسب إجمالي الشرائح المحجوزة لكل مرفق، وأعد فقط الصفوف التي يساوي فيها عدد الشرائح المحجوزة عددها لدى الأفضل'. ألن يكون أجمل لو أمكن القول 'احسب عدد الشرائح المحجوزة لكل مرفق، ورتّبها، واختر أي مرفق في المرتبة 1'؟
ولحسن الحظ، تتيح لنا دوال النافذة فعل ذلك — وإن كان من الإنصاف القول إن ذلك ليس بديهيًا للعين غير المدرّبة. وأول معلومة أساسية هي وجود دالة RANK. فهي ترتّب القيم بناءً على ORDER BY الممرَّرة إليها. وإن كان هناك تعادل على (مثلًا) المركز الثاني، فسيأخذ التالي المرتبة 4. إذن، ما نحتاج إلى فعله هو الحصول على عدد الشرائح لكل مرفق، وترتيبها، وانتقاء تلك في المرتبة الأولى. وقد تبدو المحاولة الأولى لذلك على شيء مثل ما يلي:
select facid, total from (
select facid, total, rank() over (order by total desc) rank from (
select facid, sum(slots) total
from cd.bookings
group by facid
) as sumslots
) as ranked
where rank = 1
يحسب الاستعلام الداخلي إجمالي الشرائح المحجوزة، ويرتّبها الأوسط، وينتقي الخارجي الأعلى مرتبة. ويمكننا في الحقيقة ترتيب ذلك قليلًا: تذكّر أن دوال النافذة تُطبَّق متأخرة إلى حد كبير في دالة select، أي بعد التجميع. وهذا يعني أنه يمكننا نقل التجميع إلى جزء ORDER BY من الدالة، كما هو معروض في الإجابة المعتمدة.
ورغم أن مقاربة دالة النافذة ليست أبسط كثيرًا من حيث عدد أسطر الشيفرة، فهي على الأرجح أكثر منطقية دلاليًا.
تلميح: هذا التمرين صعب قليلًا. ستحتاج إلى دالة النافذة RANK، ومن الجدير بالذكر أنه يمكن استخدام دالة تجميعية داخل عبارة ORDER BY في دالة نافذة.
18. ترتيب الأعضاء بحسب الساعات (المقرّبة) المستخدمة
السؤال
أنتج قائمة بالأعضاء (بمن فيهم الضيوف)، مع عدد الساعات التي حجزوها في المرافق، مقرّبة إلى أقرب عشر ساعات. ورتّبهم بحسب هذا الرقم المقرّب، مع إخراج الاسم الأول والاسم العائلي والساعات المقرّبة والمرتبة. ورتّب النتائج بحسب المرتبة ثم الاسم العائلي ثم الاسم الأول.
النتائج المتوقعة
| firstname | surname | hours | rank |
|---|---|---|---|
| GUEST | GUEST | 1200 | 1 |
| Darren | Smith | 340 | 2 |
| Tim | Rownam | 330 | 3 |
| Tim | Boothe | 220 | 4 |
| Tracy | Smith | 220 | 4 |
| Gerald | Butters | 210 | 6 |
| Burton | Tracy | 180 | 7 |
| Charles | Owen | 170 | 8 |
| Janice | Joplette | 160 | 9 |
| Anne | Baker | 150 | 10 |
| Timothy | Baker | 150 | 10 |
| David | Jones | 150 | 10 |
| Nancy | Dare | 130 | 13 |
| Florence | Bader | 120 | 14 |
| Anna | Mackenzie | 120 | 14 |
| Ponder | Stibbons | 120 | 14 |
| Jack | Smith | 110 | 17 |
| Jemima | Farrell | 90 | 18 |
| David | Pinker | 80 | 19 |
| Ramnaresh | Sarwin | 80 | 19 |
| Matthew | Genting | 70 | 21 |
| Joan | Coplin | 50 | 22 |
| David | Farrell | 30 | 23 |
| Henry | Worthington-Smyth | 30 | 23 |
| John | Hunt | 20 | 25 |
| Douglas | Jones | 20 | 25 |
| Millicent | Purview | 20 | 25 |
| Henrietta | Rumney | 20 | 25 |
| Erica | Crumpet | 10 | 29 |
| Hyacinth | Tupperware | 10 | 29 |
الإجابة
select firstname, surname,
((sum(bks.slots)+10)/20)*10 as hours,
rank() over (order by ((sum(bks.slots)+10)/20)*10 desc) as rank
from cd.bookings bks
inner join cd.members mems
on bks.memid = mems.memid
group by mems.memid
order by rank, surname, firstname;
لا تشكّل هذه الإجابة زيادة كبيرة على تمريننا السابق، وإن كانت توضح وظيفة RANK على نحو أفضل. ويمكنك أن ترى أن بعض مرتادي النادي لهم عدد مقرّب متساوٍ من الساعات المحجوزة، ومرتبتهم هي نفسها. وإذا تقاسم عضوان المركز الثاني، فالذي يليهما يأخذ المركز 4. وهناك دالة أخرى، هي DENSE_RANK، كانت ستعطي ذلك العضو المركز 3 بدلًا من ذلك.
ومن الجدير بالذكر التقنية التي نستخدمها للتقريب هنا. فإضافة 5 ثم القسمة على 10 ثم الضرب في 10 لها أثر (بفضل قطع الحساب الصحيح للكسور) تقريب الرقم إلى أقرب 10. وفي حالتنا، لأن الشرائح مدتها نصف ساعة، نحتاج إلى إضافة 10 ثم القسمة على 20 ثم الضرب في 10. ويمكن للمرء بالتأكيد أن يحتج بأن علينا إجراء التحويل من الشرائح إلى الساعات بصورة مستقلة عن التقريب، وهو ما يزيد الوضوح.
وبحديثنا عن الوضوح، بدأ هذا التقريب يُدخل قدرًا ملحوظًا من تكرار الشيفرة. والأمر عند هذه النقطة موكول إلى التقدير، لكن قد ترغب في إخراجه إلى استعلام فرعي كما يلي:
select firstname, surname, hours, rank() over (order by hours desc) from
(select firstname, surname,
((sum(bks.slots)+10)/20)*10 as hours
from cd.bookings bks
inner join cd.members mems
on bks.memid = mems.memid
group by mems.memid
) as subq
order by rank, surname, firstname;
تلميح: ستحتاج إلى دالة النافذة RANK مرة أخرى. ويمكنك استخدام الحساب الصحيح لإنجاز التقريب.
19. إيجاد أفضل ثلاثة مرافق من حيث توليد الإيراد
السؤال
أنتج قائمة بأفضل ثلاثة مرافق من حيث توليد الإيراد (مع التعادلات). وأخرج اسم المرفق والمرتبة، مرتّبين بحسب المرتبة ثم اسم المرفق.
النتائج المتوقعة
| name | rank |
|---|---|
| Massage Room 1 | 1 |
| Massage Room 2 | 2 |
| Tennis Court 2 | 3 |
الإجابة
select name, rank from (
select facs.name as name, rank() over (order by sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) desc) as rank
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.name
) as subq
where rank <= 3
order by rank;
لا يقدّم هذا السؤال أي مفاهيم جديدة، وهو مقصود فقط لمنحك فرصة التدرب على ما تعرفه بالفعل. فنستخدم عبارة CASE لحساب إيراد كل شريحة، ونجمّع ذلك لكل مرفق باستخدام SUM. ثم نستخدم دالة النافذة RANK لإنتاج ترتيب، ونلفّ الأمر كله في استعلام فرعي، ونستخرج كل ما مرتبته أقل من أو تساوي 3.
تلميح: سؤال آخر قائم على دالة النافذة RANK! تذكّر التعقيد النسبي في حساب إيراد مرفق، إذ يلزمك مراعاة التكاليف المختلفة لمستخدم الضيوف..
20. تصنيف المرافق بحسب القيمة
السؤال
صنّف المرافق إلى مجموعات متساوية الحجم: مرتفعة ومتوسطة ومنخفضة، بناءً على إيرادها. ورتّب بحسب التصنيف ثم اسم المرفق.
النتائج المتوقعة
| name | revenue |
|---|---|
| Massage Room 1 | high |
| Massage Room 2 | high |
| Tennis Court 2 | high |
| Badminton Court | average |
| Squash Court | average |
| Tennis Court 1 | average |
| Pool Table | low |
| Snooker Table | low |
| Table Tennis | low |
الإجابة
select name, case when class=1 then 'high'
when class=2 then 'average'
else 'low'
end revenue
from (
select facs.name as name, ntile(3) over (order by sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) desc) as class
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.name
) as subq
order by class, name;
ينبغي أن يستخدم هذا التمرين في معظمه مفاهيم مألوفة، وإن كنا نقدّم دالة النافذة NTILE. تجمع NTILE القيم في عدد من المجموعات ممرَّر إليها، بأكبر تساوٍ ممكن. وتُخرج رقمًا من 1 إلى عدد المجموعات. ثم نستخدم عبارة CASE لتحويل ذلك الرقم إلى تصنيف!
تلميح: ابحث في دالة النافذة NTILE.
21. حساب زمن استرداد التكلفة لكل مرفق
السؤال
بناءً على الأشهر الثلاثة الكاملة من البيانات حتى الآن، احسب مقدار الوقت الذي سيستغرقه كل مرفق لاسترداد تكلفة تملكه. وتذكّر أن تراعي الصيانة الشهرية المستمرة. وأخرج اسم المرفق وزمن الاسترداد بالأشهر، مرتّبًا بحسب اسم المرفق. ولا تقلق من اختلاف أطوال الأشهر، فنحن نبحث هنا عن قيمة تقريبية فقط!
النتائج المتوقعة
| name | months |
|---|---|
| Badminton Court | 6.8317677198975235 |
| Massage Room 1 | 0.18885741265344664778 |
| Massage Room 2 | 1.7621145374449339 |
| Pool Table | 5.3333333333333333 |
| Snooker Table | 6.9230769230769231 |
| Squash Court | 1.1339582703356516 |
| Table Tennis | 6.4000000000000000 |
| Tennis Court 1 | 2.2624434389140271 |
| Tennis Court 2 | 1.7505470459518600 |
الإجابة
select facs.name as name,
facs.initialoutlay/((sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end)/3) - facs.monthlymaintenance) as months
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.facid
order by name;
على النقيض من كل تماريننا الأخيرة، لا حاجة هنا إلى استخدام دوال النافذة لحلّ هذه المسألة: فالأمر مجرد قليل من الحساب يشمل الإيراد الشهري والنفقة الأولية والصيانة الشهرية. ومرة أخرى، في شيفرة الإنتاج قد تريد توضيح ما يجري هنا قليلًا باستخدام استعلام فرعي (وإن كان إدخال ذلك في الإنتاج غير مرجّح، لأننا ثبّتنا عدد الأشهر في الشيفرة!). وقد تبدو نسخة منظّفة كما يلي:
select name,
initialoutlay / (monthlyrevenue - monthlymaintenance) as repaytime
from
(select facs.name as name,
facs.initialoutlay as initialoutlay,
facs.monthlymaintenance as monthlymaintenance,
sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end)/3 as monthlyrevenue
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by facs.facid
) as subq
order by name;
لكن، أسمعك تسأل: كيف ستبدو نسخة تلقائية من هذا؟ نسخة لا تحتاج إلى عدد أشهر مثبّت في الشيفرة؟ هذا أكثر تعقيدًا قليلًا، ويتضمن بعض الحساب على التواريخ. وقد أخرجته إلى تعبير CTE ليكون أوضح قليلًا.
with monthdata as (
select mincompletemonth,
maxcompletemonth,
(extract(year from maxcompletemonth)*12) +
extract(month from maxcompletemonth) -
(extract(year from mincompletemonth)*12) -
extract(month from mincompletemonth) as nummonths
from (
select date_trunc('month',
(select max(starttime) from cd.bookings)) as maxcompletemonth,
date_trunc('month',
(select min(starttime) from cd.bookings)) as mincompletemonth
) as subq
)
select name,
initialoutlay / (monthlyrevenue - monthlymaintenance) as repaytime
from
(select facs.name as name,
facs.initialoutlay as initialoutlay,
facs.monthlymaintenance as monthlymaintenance,
sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end)/(select nummonths from monthdata) as monthlyrevenue
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
where bks.starttime < (select maxcompletemonth from monthdata)
group by facs.facid
) as subq
order by name;
تقيّد هذه الشيفرة البيانات الداخلة بالأشهر الكاملة. وتفعل ذلك باختيار أكبر تاريخ، وتقريبه نزولًا إلى الشهر، وإزالة كل التواريخ الأكبر من ذلك. وحتى هذه الشيفرة ليست كاملة تمامًا. فهي لا تتعامل مع حالة تكبّد مرفق خسارة. وإصلاح ذلك ليس صعبًا كثيرًا، وقد تُرك (كتمرين آخر) للقارئ!
تلميح: لا حاجة إلى استخدام دوال النافذة لحلّ هذه المسألة. ثبّت عدد الأشهر في الشيفرة ليسهل الأمر، أو احسبه لتصعبه.
22. حساب متوسط متحرك للإيراد الإجمالي
السؤال
لكل يوم في أغسطس 2012، احسب متوسطًا متحركًا للإيراد الإجمالي على مدى الأيام الخمسة عشر السابقة. وينبغي أن يحتوي الإخراج على عمودَي date وrevenue، مرتّبين بحسب التاريخ. وتذكّر أن تراعي احتمال أن يكون إيراد يوم ما صفرًا. هذا التمرين صعب قليلًا، فلا تخف من الاطلاع على التلميح!
النتائج المتوقعة
| date | revenue |
|---|---|
| 2012-08-01 | 1126.8333333333333333 |
| 2012-08-02 | 1153.0000000000000000 |
| 2012-08-03 | 1162.9000000000000000 |
| 2012-08-04 | 1177.3666666666666667 |
| 2012-08-05 | 1160.9333333333333333 |
| 2012-08-06 | 1185.4000000000000000 |
| 2012-08-07 | 1182.8666666666666667 |
| 2012-08-08 | 1172.6000000000000000 |
| 2012-08-09 | 1152.4666666666666667 |
| 2012-08-10 | 1175.0333333333333333 |
| 2012-08-11 | 1176.6333333333333333 |
| 2012-08-12 | 1195.6666666666666667 |
| 2012-08-13 | 1218.0000000000000000 |
| 2012-08-14 | 1247.4666666666666667 |
| 2012-08-15 | 1274.1000000000000000 |
| 2012-08-16 | 1281.2333333333333333 |
| 2012-08-17 | 1324.4666666666666667 |
| 2012-08-18 | 1373.7333333333333333 |
| 2012-08-19 | 1406.0666666666666667 |
| 2012-08-20 | 1427.0666666666666667 |
| 2012-08-21 | 1450.3333333333333333 |
| 2012-08-22 | 1539.7000000000000000 |
| 2012-08-23 | 1567.3000000000000000 |
| 2012-08-24 | 1592.3333333333333333 |
| 2012-08-25 | 1615.0333333333333333 |
| 2012-08-26 | 1631.2000000000000000 |
| 2012-08-27 | 1659.4333333333333333 |
| 2012-08-28 | 1687.0000000000000000 |
| 2012-08-29 | 1684.6333333333333333 |
| 2012-08-30 | 1657.9333333333333333 |
| 2012-08-31 | 1703.4000000000000000 |
الإجابة
select dategen.date,
(
-- correlated subquery that, for each day fed into it,
-- finds the average revenue for the last 15 days
select sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) as rev
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
where bks.starttime > dategen.date - interval '14 days'
and bks.starttime < dategen.date + interval '1 day'
)/15 as revenue
from
(
-- generates a list of days in august
select cast(generate_series(timestamp '2012-08-01',
'2012-08-31','1 day') as date) as date
) as dategen
order by dategen.date;
هناك على الأقل حلّان جيدان بالقدر نفسه لهذا السؤال. وقد وضعت أبسطهما كتابةً كإجابة، لكن هناك أيضًا حلًا أكثر مرونة يستخدم دوال النافذة.
لننظر إلى الإجابة المختارة أولًا. حين أقرأ استعلامات SQL، أميل إلى قراءة جزء SELECT أخيرًا — فجزآ FROM وWHERE يميلان إلى مزيد من الإثارة. فما لدينا في FROM؟ نداء لدالة GENERATE_SERIES. وهي تفعل إلى حد كبير ما تقوله حرفيًا — تولّد سلسلة قيم. ويمكنك تحديد قيمة بداية وقيمة توقف وقيمة زيادة. وهي تعمل مع الأنواع الصحيحة والتواريخ — وإن كنا، كما ترى، نحتاج إلى أن نكون صريحين بشأن الأنواع الداخلة إلى الدالة والخارجة منها. حاول إزالة التحويلات وترى النتيجة!
إذن، ولّدنا طابعًا زمنيًا لكل يوم في أغسطس. والآن، نحتاج لكل يوم إلى توليد متوسطنا. ويمكننا فعل ذلك باستخدام استعلام فرعي مترابط. فإن كنت تتذكر، الاستعلام الفرعي المترابط استعلام فرعي يستخدم قيمًا من الاستعلام الخارجي. وهذا يعني أنه يُنفَّذ مرة واحدة لكل صف نتيجة في الاستعلام الخارجي. ويقابله الاستعلام الفرعي غير المترابط، الذي لا يلزم تنفيذه إلا مرة واحدة.
وإذا نظرنا إلى استعلامنا الفرعي المترابط، نرى أنه مترابط على حقل dategen.date. فهو ينتج مجموع الإيراد لهذا اليوم والأيام الأربعة عشر السابقة له، ثم يقسم ذلك المجموع على 15. وهذا ينتج الإخراج الذي نبحث عنه!
ذكرت أن هناك حلًا قائمًا على دوال النافذة لهذه المسألة أيضًا — وتراه أدناه. والمقاربة التي نستخدمها لذلك توليد قائمة بالإيراد لكل يوم، ثم استخدام تجميع دوال النافذة على تلك القائمة. والجميل في هذه الطريقة أنه متى كانت لديك الإيرادات اليومية، أمكنك إنتاج مجموعة واسعة من النتائج بسهولة تامة — فقد تريد، مثلًا، متوسطات متحركة للشهر السابق و15 يومًا و5 أيام. ويسهل فعل ذلك بهذه الطريقة، ويصعب نسبيًا بالتجميع التقليدي.
select date, avgrev from (
-- AVG over this row and the 14 rows before it.
select dategen.date as date,
avg(revdata.rev) over(order by dategen.date rows 14 preceding) as avgrev
from
-- generate a list of days. This ensures that a row gets generated
-- even if the day has 0 revenue. Note that we generate days before
-- the start of october - this is because our window function needs
-- to know the revenue for those days for its calculations.
(select
cast(generate_series(timestamp '2012-07-10', '2012-08-31','1 day') as date) as date
) as dategen
left outer join
-- left join to a table of per-day revenue
(select cast(bks.starttime as date) as date,
sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) as rev
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by cast(bks.starttime as date)
) as revdata
on dategen.date = revdata.date
) as subq
where date >= '2012-08-01'
order by date;
ستلاحظ أننا كنا نريد حساب الإيراد اليومي كثيرًا. وبدلًا من إدراج ذلك الحساب في كل استعلاماتنا، وهو أمر غير مرتب إلى حد كبير (وسيسبب لنا صداعًا كبيرًا إن غيّرنا مخططنا يومًا)، نريد على الأرجح تخزين تلك المعلومة في مكان ما. وقد تكون فكرتك الأولى حساب المعلومة وتخزينها في مكان ما للاستخدام لاحقًا. وهذا تكتيك شائع لمستودعات البيانات الكبيرة، لكنه قد يسبب لنا بعض المشكلات — فإذا عدنا يومًا وحرّرنا بياناتنا، فعلينا أن نتذكر إعادة الحساب. وبالنسبة لبيانات ليست هائلة الحجم كالتي ننظر فيها هنا، يمكننا ببساطة إنشاء عرض (view) بدلًا من ذلك. والعرض في جوهره استعلام مخزّن يشبه جدولًا تمامًا. وفي الخفاء، يستبدل نظام إدارة قواعد البيانات الجزء ذا الصلة من تعريف العرض عند اختيارك بيانات منه. وإنشاء العروض سهل جدًا، كما ترى أدناه:
create or replace view cd.dailyrevenue as
select cast(bks.starttime as date) as date,
sum(case
when memid = 0 then slots * facs.guestcost
else slots * membercost
end) as rev
from cd.bookings bks
inner join cd.facilities facs
on bks.facid = facs.facid
group by cast(bks.starttime as date);
ويمكنك أن ترى أن هذا يجعل استعلامنا أبسط كثيرًا!
select date, avgrev from (
select dategen.date as date,
avg(revdata.rev) over(order by dategen.date rows 14 preceding) as avgrev
from
(select
cast(generate_series(timestamp '2012-07-10', '2012-08-31','1 day') as date) as date
) as dategen
left outer join
cd.dailyrevenue as revdata on dategen.date = revdata.date
) as subq
where date >= '2012-08-01'
order by date;
وإلى جانب تخزين أجزاء الاستعلامات كثيرة الاستخدام، يمكن استخدام العروض لأغراض متنوعة، منها تقييد الوصول إلى أعمدة معيّنة من جدول.
تلميح: ستحتاج إلى توليد قائمة أيام: راجع GENERATE_SERIES لذلك. وبعدها يمكنك حلّ هذه المسألة باستخدام دوال التجميع أو دوال النافذة.