الوحدة 4 · SQL في العالم الحقيقي
SQL مقروء وفروق اللهجات
15 دقراءة 4 دتمرين SQL
الدرس في لمحة
نسّق الـ queries بحيث يستطيع المراجع فحصها بسرعة، وانقل عادات SQLite التي تعلّمتها في هذه الدورة إلى PostgreSQL بمعرفة مواضع الاختلاف في التواريخ والقيم المنطقية وLIKE وقواعد التجميع.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يارا، المحلّلة الأقدم في فريقك، تراجع كل query قبل أن يصل إلى صاحب الطلب. نادرًا ما يكون تعليقها الأول على عمل المحلّلين الجدد عن المنطق. إنه «لا أستطيع فحص هذا بسرعة». أما تعليقها الثاني هذا الربع فعملي: Cartwheel تنقل تقاريرها إلى مستودع بيانات على PostgreSQL، وكل query كتبته يجب أن ينجو من هذا الانتقال. يغطي هذا الدرس الأمرين: SQL يستطيع المراجعون قراءته، وSQL ينتقل بين المحرّكات.
query على شخص آخر أن يفحصه
هذا query حقيقي من لوحة المعلومات القديمة لفريق الدعم. إنه يعمل ويعيد أرقامًا معقولة:
select t.channel,count(*),avg(satisfaction) from support_tickets t,customers c where t.customer_id=c.customer_id and closed_at is not null and opened_at>='2025-01-01' group by 1 order by 3 desc;على المراجع أن يجتهد ليجيب عن أسئلة أساسية. لماذا customers هنا؟ (إنه غير مستخدم؛ بقي من نسخة سابقة، وهو الآن JOIN على أحدهم أن يفكّر فيه.) هل هذا شرط ربط أم شرط تصفية؟ إلى أي جدول ينتمي satisfaction؟ ما العمود 3؟ هل ينقص شرط 2025 حدٌّ أعلى أم أن ذلك مقصود؟ وهذا التحليل نفسه مكتوبًا ليُفحص:
-- Support satisfaction by contact channel, tickets opened in 2025.
-- grain: one row per channel. Only closed tickets: open ones can't be rated yet.
SELECT
t.channel,
COUNT(*) AS closed_tickets,
COUNT(t.satisfaction) AS rated,
ROUND(AVG(t.satisfaction), 2) AS avg_satisfaction
FROM support_tickets AS t
WHERE t.closed_at IS NOT NULL
AND t.opened_at >= '2025-01-01'
AND t.opened_at < '2026-01-01'
GROUP BY t.channel
ORDER BY avg_satisfaction DESC;التغييرات، مرتّبة حسب أهميتها:
- احذف ما لا يُستخدم. اختفى الـ JOIN غير المستخدم. كل جدول في FROM احتمال fan-out أو شرط تصفية متنكّر، فعليه أن يستحق مكانه.
- ربط صريح. حين تربط فعلًا، اكتب
JOIN ... ON، ولا تكتب أبدًا فاصلة مع شرط في WHERE. عندها يقع شرط الربط بجانب الجدول الذي ينتمي إليه. - الأسماء بدل المواضع. لكل عمود في النتيجة اسم، ويستخدمه ORDER BY.
GROUP BY 1تسهيل مختلَف عليه؛ أما الأسماء فتصمد أمام تعديلات قائمة SELECT. - عمود واحد في كل سطر، منسوب إلى اسم مستعار حسب الدور. فتصبح الفروق في مراجعة الكود سطرًا واحدًا لكل تغيير.
- تعليقات تقول لماذا. يذكر التعليق في رأس الـ query السؤال ومستوى التفصيل؛ و«التذاكر المغلقة فقط» تشرح قرارًا. لا تعلّق على ما يقوله الـ SQL بوضوح.
- اعرض المقام. يقع
ratedبجانب المتوسط، وهو درس معدل الاستجابة من القسم 1.
الكلمات المفتاحية بأحرف كبيرة، والفواصل في بداية السطر أو نهايته، وعرض المسافات البادئة، كلها أعراف فريق. اختر واحدًا، وطبّقه في كل مكان، ودع أداة تنسيق تفرضه. الاتساق أهم بكثير من الأسلوب الذي يفوز.
من SQLite إلى PostgreSQL
SQL معيار، لكن لكل محرّك دواله الخاصة ودرجة تساهله الخاصة. كل ما في هذه الدورة في الغالب يعمل دون تغيير في PostgreSQL: الـ JOIN، والـ CTEs، والـ CTEs التكرارية، ودوال النافذة، وFILTER، وNULLIF، وCOALESCE، وIS DISTINCT FROM. وتتجمّع الفروق في مواضع قليلة:
| المهمة | SQLite | PostgreSQL |
|---|---|---|
| تسمية الشهر | strftime('%Y-%m', d) |
to_char(d, 'YYYY-MM') |
| بداية الشهر | date(d, 'start of month') |
date_trunc('month', d) |
| إضافة فترة زمنية | date(d, '+7 days') |
d + INTERVAL '7 days' |
| الساعات بين لحظتين | (julianday(b) - julianday(a)) * 24 |
EXTRACT(EPOCH FROM b - a) / 3600 |
| مطابقة غير حسّاسة لحالة الأحرف | LIKE (ASCII) |
ILIKE |
| الاختيار بين احتمالين | IIF(c, a, b) أو CASE |
CASE |
| عدّ شرط | SUM(cond) أو FILTER |
COUNT(*) FILTER (WHERE cond) |
التغيير الأكبر يقع تحت الجدول. لدى PostgreSQL نوعا date وtimestamp حقيقيان، فتُقارَن أعمدة التاريخ كتواريخ، وتعيد العمليات الحسابية على التواريخ فترات زمنية (intervals) لا أرقامًا. والقيم المنطقية نوع حقيقي أيضًا: SUM(status = 'cancelled') يفشل هناك لأنك لا تستطيع جمع قيمة منطقية، ولهذا يكون FILTER هو الخيار القابل للنقل.
فرقان أصغر يوقعان الناس باستمرار. القسمة الصحيحة تقتطع الكسور في المحرّكين (7 / 2 تساوي 3)، فاستمر في كتابة 100.0 * للنسب المئوية. ويضع PostgreSQL قيم NULL أخيرًا في الترتيب التصاعدي بينما يضعها SQLite أولًا، فأضف NULLS FIRST أو NULLS LAST كلما كان موضع NULL مهمًا.
التمرين مهمة نقل حقيقية: query من مستودع البيانات يفشل في SQLite. بعد ذلك، يجمع المشروع الختامي الدورة كلها.
أهم النقاط
- الـ SQL المقروء يستخدم JOIN … ON صريحًا، وعمودًا واحدًا في كل سطر، وأسماء مستعارة حسب الأدوار، وخطوات CTE مسمّاة، وتعليقات تشرح لماذا لا ماذا.
- احذف ما لا يستخدمه الـ query، مثل عمليات الربط غير المستخدمة وSELECT *، لأن كل جدول إضافي احتمال fan-out وسؤال على المراجع.
- التعامل مع التواريخ هو أكبر فرق بين SQLite وPostgreSQL؛ فـ strftime وjulianday تصبحان date_trunc وto_char وفترات زمنية (intervals) وأنواع تاريخ حقيقية.
- SQLite أكثر تساهلًا من PostgreSQL (الأعمدة المجرّدة، وLIKE غير الحسّاس لحالة الأحرف، والقيم المنطقية كـ 0/1)، فقد يفشل query يعمل في SQLite أو يتصرّف بشكل مختلف في PostgreSQL.