EduFlow
SQL للمحلّلين
⁦0%⁩

الوحدة 4 · SQL في العالم الحقيقي

SQL مقروء وفروق اللهجات

15 دقراءة 4 دتمرين SQL

الدرس في لمحة

نسّق الـ queries بحيث يستطيع المراجع فحصها بسرعة، وانقل عادات SQLite التي تعلّمتها في هذه الدورة إلى PostgreSQL بمعرفة مواضع الاختلاف في التواريخ والقيم المنطقية وLIKE وقواعد التجميع.

  1. 1query على شخص آخر أن يفحصه
  2. 2من SQLite إلى PostgreSQL

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

يارا، المحلّلة الأقدم في فريقك، تراجع كل query قبل أن يصل إلى صاحب الطلب. نادرًا ما يكون تعليقها الأول على عمل المحلّلين الجدد عن المنطق. إنه «لا أستطيع فحص هذا بسرعة». أما تعليقها الثاني هذا الربع فعملي: Cartwheel تنقل تقاريرها إلى مستودع بيانات على PostgreSQL، وكل query كتبته يجب أن ينجو من هذا الانتقال. يغطي هذا الدرس الأمرين: SQL يستطيع المراجعون قراءته، وSQL ينتقل بين المحرّكات.

query على شخص آخر أن يفحصه

هذا query حقيقي من لوحة المعلومات القديمة لفريق الدعم. إنه يعمل ويعيد أرقامًا معقولة:

SQL
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 حدٌّ أعلى أم أن ذلك مقصود؟ وهذا التحليل نفسه مكتوبًا ليُفحص:

SQL
-- 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;

التغييرات، مرتّبة حسب أهميتها:

  1. احذف ما لا يُستخدم. اختفى الـ JOIN غير المستخدم. كل جدول في FROM احتمال fan-out أو شرط تصفية متنكّر، فعليه أن يستحق مكانه.
  2. ربط صريح. حين تربط فعلًا، اكتب JOIN ... ON، ولا تكتب أبدًا فاصلة مع شرط في WHERE. عندها يقع شرط الربط بجانب الجدول الذي ينتمي إليه.
  3. الأسماء بدل المواضع. لكل عمود في النتيجة اسم، ويستخدمه ORDER BY. GROUP BY 1 تسهيل مختلَف عليه؛ أما الأسماء فتصمد أمام تعديلات قائمة SELECT.
  4. عمود واحد في كل سطر، منسوب إلى اسم مستعار حسب الدور. فتصبح الفروق في مراجعة الكود سطرًا واحدًا لكل تغيير.
  5. تعليقات تقول لماذا. يذكر التعليق في رأس الـ query السؤال ومستوى التفصيل؛ و«التذاكر المغلقة فقط» تشرح قرارًا. لا تعلّق على ما يقوله الـ SQL بوضوح.
  6. اعرض المقام. يقع 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.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تظهر العبارة FROM support_tickets t, customers c WHERE t.customer_id = c.customer_id في query لا يستخدم أي عمود من c. إطلاقًا. ماذا يجب أن يطلب المراجع؟

السؤال 2 من 4

أي تعبير في SQLite له مقابل مباشر في PostgreSQL هو date_trunc('month', order_date)؟

السؤال 3 من 4

هذا يعمل في SQLite: SELECT channel, status, COUNT(*) FROM orders GROUP BY channel. ماذا يحدث في PostgreSQL؟

السؤال 4 من 4

يجد WHERE email LIKE '%@EXAMPLE.COM' كل العملاء الـ 600 في SQLite. كم عميلًا يجد في PostgreSQL، حيث تُخزَّن عناوين البريد بأحرف صغيرة؟

تدريب

تمرين SQL

انقل query من مستودع البيانات إلى SQLite

وجدت هدى هذا الـ query في مستودع البيانات على PostgreSQL وتريد الأرقام نفسها من قاعدة بيانات SQLite الخاصة بـ Cartwheel: للعملاء في دبي، صف واحد لكل شهر في 2025 فيه month (‘YYYY-MM’) وorders (الطلبات غير الملغاة) وapp_orders (الطلبات التي قُدّمت عبر تطبيق الجوال). رتّب حسب month. الترتيب مهم.

الكود المبدئي يفشل في SQLite. ترجم كل جزء خاص بـ PostgreSQL إلى SQLite وأبقِ المنطق كما هو.

الجداول
customers
(customer_id, first_name, last_name, email, country, city, signup_date, segment, referred_by)
orders
(order_id, customer_id, order_date, status, channel, coupon_code, shipping_fee)
order_items
(order_item_id, order_id, product_id, quantity, unit_price, discount)
products
(product_id, name, category_id, unit_price, unit_cost, launched_on, is_active)
categories
(category_id, name, parent_id)
returns
(return_id, order_item_id, return_date, reason, refund_amount)
employees
(employee_id, first_name, last_name, title, department, manager_id, hire_date, salary)
support_tickets
(ticket_id, customer_id, order_id, opened_at, closed_at, priority, channel, satisfaction, agent_id)
web_sessions
(session_id, customer_id, started_at, source, device, pages_viewed, added_to_cart, purchased)
-- Pasted from the PostgreSQL warehouse. It fails in SQLite: port it.
-- grain: one row per month, Dubai customers, 2025, non-cancelled orders
SELECT
  to_char(date_trunc('month', o.order_date::timestamp), 'YYYY-MM') AS month,
  COUNT(*) AS orders,
  COUNT(*) FILTER (WHERE o.channel ILIKE 'MOBILE%') AS app_orders
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
WHERE c.city ILIKE 'dubai'
  AND o.status <> 'cancelled'
  AND o.order_date >= DATE '2025-01-01'
  AND o.order_date < DATE '2026-01-01'
GROUP BY month
ORDER BY month;جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.