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

الوحدة 1 · الاستعلام بهدف واضح

التعامل مع التواريخ في SQLite

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

الدرس في لمحة

صفِّ الزمن وجمّعه وقِسه في SQLite باستخدام نصوص التواريخ بصيغة ISO، والنطاقات نصف المفتوحة، وstrftime() لأجزاء التاريخ، وjulianday() للمُدد، دون أن تفقد آخر يوم في الفترة.

  1. 1التواريخ نصوص، ولا بأس في ذلك
  2. 2تفكيك التواريخ
  3. 3النوافذ المتحركة وأيام الأسبوع
  4. 4قياس المُدد

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

يُعدّ مارك إيفانز، نائب الرئيس للعمليات، مراجعة لشهر نوفمبر. يريد رقمين: كم طلبًا استقبلت Cartwheel في نوفمبر 2025، وبأي سرعة حلّ فريق الدعم التذاكر. كلاهما سؤال عن التواريخ، وSQLite يتعامل مع التواريخ بطريقة تختلف عن معظم قواعد البيانات. حين تفهم طريقته، تجدها بسيطة ويمكن التنبؤ بها.

التواريخ نصوص، ولا بأس في ذلك

لا يملك SQLite نوعًا مستقلًا للتواريخ. تخزّن Cartwheel العمود order_date كنص بصيغة ISO-8601: '2025-11-28 19:55:30'. ولأن الصيغة تبدأ بالوحدة الأكبر وتنتهي بالأصغر، فإن الترتيب الأبجدي هو الترتيب الزمني. مقارنة النصوص تقارن لحظات زمنية، والـ index على العمود يعمل مع نطاقات التواريخ.

هذا هو الـ query البديهي لشهر نوفمبر:

SQL
SELECT COUNT(*) AS november_orders
FROM orders
WHERE order_date BETWEEN '2025-11-01' AND '2025-11-30';

336 طلبًا. وهو خاطئ بفارق 11.

خط زمني من 29 نوفمبر إلى 1 ديسمبر. الشرط BETWEEN '2025-11-01' AND '2025-11-30' يتوقف عند منتصف الليل في بداية 30 نوفمبر، فيفوّت طلبًا في الساعة 19:55 من ذلك اليوم. أما النطاق نصف المفتوح، أصغر من '2025-12-01'، فيغطي 30 نوفمبر كله. 11-29 11-30 00:00 12-01 00:00 طلب 19:55 BETWEEN … AND '2025-11-30' >= '2025-11-01' AND < '2025-12-01' الزمن يتقدّم يمينًا
الحدّ الأعلى بلا وقت يعني منتصف الليل في بداية اليوم الأخير

النص '2025-11-30' أقصر من '2025-11-30 19:55:30'، والنص الأقصر الذي يطابق البداية يأتي أولًا في الترتيب. لذا فالحدّ الأعلى فعليًا هو منتصف الليل في بداية 30 نوفمبر، وكل ما بعده في ذلك اليوم يقع خارج النطاق. الإصلاح هو النطاق نصف المفتوح: اشمل البداية، واستبعد بداية الفترة التالية.

SQL
SELECT COUNT(*) AS november_orders
FROM orders
WHERE order_date >= '2025-11-01'
  AND order_date <  '2025-12-01';

347، وهو الرقم الحقيقي. في أكثر أيام عطلة نهاية أسبوع Black Friday ازدحامًا، كانت هذه الطلبات الـ 11 المفقودة ستترك فجوة واضحة في رسم مارك البياني.

تفكيك التواريخ

تعيد strftime(format, value) أجزاء التاريخ كنص:

الصيغة تعيد مثال
'%Y-%m' السنة والشهر '2025-11'
'%Y' السنة '2025'
'%w' يوم الأسبوع، الأحد = 0 '5'
'%H' الساعة، 00–23 '19'

'%Y-%m' هي الصيغة الأكثر استخدامًا: تحوّل الطابع الزمني إلى تسمية شهر يمكنك التجميع حسبها في الدرس التالي. ولأن النتيجة نص، قارنها بنص: strftime('%w', order_date) = '0' يجد طلبات يوم الأحد، بينما = 0 لا يجد شيئًا.

تُوحّد date() وdatetime() صيغة القيمة وتقبلان مُعدِّلات (modifiers) تجعل حدود الفترات سهلة:

SQL
SELECT
  date('2025-11-28 19:55:30')                                   AS just_the_date,
  date('2025-11-28', 'start of month')                          AS month_start,
  date('2025-11-28', 'start of month', '+1 month')              AS next_month_start,
  date('2025-11-28', 'start of month', '+1 month', '-1 day')    AS month_end,
  date('2025-11-28', '-6 days')                                 AS week_ago;

تُطبَّق المُعدِّلات من اليسار إلى اليمين. و'start of month', '+1 month' هو بالضبط الحدّ الأعلى غير المشمول الذي يحتاجه النطاق نصف المفتوح.

النوافذ المتحركة وأيام الأسبوع

«الطلبات في آخر 30 يومًا» نافذة متحركة، وتخفي سؤالًا عن الخطأ بفارق واحد: هل تشمل «آخر 30 يومًا» حتى 31 ديسمبر اليوم 31 نفسه؟ عادةً نعم، ما يعني أن النافذة تبدأ في 2 ديسمبر، قبل 29 يومًا. ابنِ الحدّين كليهما من تاريخ الارتكاز حتى تتحرك النافذة معه:

SQL
SELECT COUNT(*) AS orders_last_30_days
FROM orders
WHERE order_date >= date('2025-12-31', '-29 days')
  AND order_date <  date('2025-12-31', '+1 day');

189 طلبًا. الحدّ الأعلى هو بداية اليوم التالي لتاريخ الارتكاز، وهي فكرة النطاق نصف المفتوح نفسها. اكتب أول يوم وآخر يوم في النافذة في تعليق فوق الـ query؛ فهذا بالضبط ما سيتحقق منه من يراجعه.

يجمع تحليل أيام الأسبوع بين strftime('%w', ...) وتعبيرات CASE من الدرس السابق. ربط ‘0’ و’6’ بتسمية عطلة نهاية الأسبوع خطوة أولى شائعة حين يسأل التسويق هل تستحق حملات عطلة نهاية الأسبوع العناء:

SQL
SELECT
  order_id,
  order_date,
  CASE strftime('%w', order_date)
    WHEN '0' THEN 'weekend'
    WHEN '6' THEN 'weekend'
    ELSE 'weekday'
  END AS day_type
FROM orders
ORDER BY order_date DESC
LIMIT 5;

يستخدم هذا الـ CASE الصيغة البسيطة، CASE value WHEN x THEN ...، التي تقارن قيمة واحدة بقائمة. وهي مفيدة لعمليات الربط كهذه؛ أما الصيغة الشرطية (searched)، CASE WHEN condition THEN ...، فاستخدمها كلما احتاج فرعٌ إلى شرط حقيقي.

قياس المُدد

تحوّل julianday() التاريخ إلى عدد من الأيام (مع كسور للوقت). اطرح قيمتين فتحصل على مدة بالأيام؛ واضرب في 24 لتحصل على الساعات. هذه تذاكر ديسمبر العاجلة مع أوقات حلّها:

SQL
SELECT
  ticket_id,
  opened_at,
  closed_at,
  ROUND((julianday(closed_at) - julianday(opened_at)) * 24, 1) AS hours_to_close
FROM support_tickets
WHERE priority = 'urgent'
  AND opened_at >= '2025-12-01'
ORDER BY opened_at;

التذكرة 691 بلا closed_at، لذا مدّتها NULL. وهذا صحيح: إنها ليست صفر ساعات، بل لم تنتهِ بعد، وأي متوسط تحسبه لاحقًا سيتخطّاها بدل أن يجمّل أداء الفريق.

أصبحت قادرًا على تصفية الزمن وقياسه. أما سؤال مارك الآخر، متوسط وقت الحل لكل أولوية، فيحتاج إلى فكرة إضافية: ضغط صفوف كثيرة في صف واحد لكل مجموعة. وهذا هو الدرس التالي.

أهم النقاط

  • يخزّن SQLite التواريخ كنص بصيغة ISO-8601، وهو نص يُرتَّب زمنيًا، لذا تعمل مقارنة النصوص العادية كشرط على التاريخ.
  • صفِّ أي فترة بنطاق نصف مفتوح (>= البداية AND < بداية الفترة التالية)؛ أما BETWEEN بحدّ أعلى بلا وقت فيُسقط كل ما بعد منتصف الليل في اليوم الأخير.
  • تستخرج strftime() أجزاءً مثل الشهر (‘%Y-%m’) أو يوم الأسبوع (‘%w’، والأحد = 0) أو الساعة (‘%H’) كنص.
  • يعطي julianday(later) - julianday(earlier) مدة بالأيام مع الكسور؛ اضربها في 24 لتحصل على الساعات.
  • اربط حسابات «حتى تاريخ كذا» بتاريخ ثابت بدل ‘now’ حتى يعطي الـ query نفسه الإجابة نفسها غدًا.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

يحمل order_date قيمًا مثل ‘2025-11-30 19:55:30’. أي شرط يعيد كل طلب في نوفمبر 2025؟

السؤال 2 من 4

ماذا يعيد strftime('%m', '2025-03-09 14:00:00')؟

السؤال 3 من 4

فُتحت تذكرة في ‘2025-12-04 17:56:18’ وأُغلقت في ‘2025-12-05 01:56:18’. ماذا يعيد (julianday(closed_at) - julianday(opened_at)) * 24؟

السؤال 4 من 4

تحسب مدة بقاء العميل بـ julianday('now') - julianday(signup_date). لماذا قد يعترض زميلك؟

تدريب

تمرين SQL

أوقات الحل في نوفمبر

يريد مارك كل تذكرة فُتحت في نوفمبر 2025 مع وقت حلّها بالساعات.

أعِد ticket_id وpriority وhours_to_close (مقرّبًا إلى منزلة عشرية واحدة؛ واتركه NULL للتذاكر التي ما زالت مفتوحة)، مرتّبة حسب ticket_id. الترتيب مهم.

في الكود المبدئي خطآن: أحدهما يُسقط تذاكر، والآخر يخطئ في الوحدة.

الجداول
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)
-- November 2025 tickets and how long they took to close
SELECT
  ticket_id,
  priority,
  ROUND(julianday(closed_at) - julianday(opened_at), 1) AS hours_to_close
FROM support_tickets
WHERE opened_at BETWEEN '2025-11-01' AND '2025-11-30'
ORDER BY ticket_id;جارٍ تحميل المحرّر…

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