الوحدة 1 · الاستعلام بهدف واضح
التعامل مع التواريخ في SQLite
18 دقراءة 4 دتمرين SQL
الدرس في لمحة
صفِّ الزمن وجمّعه وقِسه في SQLite باستخدام نصوص التواريخ بصيغة ISO، والنطاقات نصف المفتوحة، وstrftime() لأجزاء التاريخ، وjulianday() للمُدد، دون أن تفقد آخر يوم في الفترة.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يُعدّ مارك إيفانز، نائب الرئيس للعمليات، مراجعة لشهر نوفمبر. يريد رقمين: كم طلبًا استقبلت Cartwheel في نوفمبر 2025، وبأي سرعة حلّ فريق الدعم التذاكر. كلاهما سؤال عن التواريخ، وSQLite يتعامل مع التواريخ بطريقة تختلف عن معظم قواعد البيانات. حين تفهم طريقته، تجدها بسيطة ويمكن التنبؤ بها.
التواريخ نصوص، ولا بأس في ذلك
لا يملك SQLite نوعًا مستقلًا للتواريخ. تخزّن Cartwheel العمود order_date كنص بصيغة ISO-8601: '2025-11-28 19:55:30'. ولأن الصيغة تبدأ بالوحدة الأكبر وتنتهي بالأصغر، فإن الترتيب الأبجدي هو الترتيب الزمني. مقارنة النصوص تقارن لحظات زمنية، والـ index على العمود يعمل مع نطاقات التواريخ.
هذا هو الـ query البديهي لشهر نوفمبر:
SELECT COUNT(*) AS november_orders
FROM orders
WHERE order_date BETWEEN '2025-11-01' AND '2025-11-30';336 طلبًا. وهو خاطئ بفارق 11.
النص '2025-11-30' أقصر من '2025-11-30 19:55:30'، والنص الأقصر الذي يطابق البداية يأتي أولًا في الترتيب. لذا فالحدّ الأعلى فعليًا هو منتصف الليل في بداية 30 نوفمبر، وكل ما بعده في ذلك اليوم يقع خارج النطاق. الإصلاح هو النطاق نصف المفتوح: اشمل البداية، واستبعد بداية الفترة التالية.
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) تجعل حدود الفترات سهلة:
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 يومًا. ابنِ الحدّين كليهما من تاريخ الارتكاز حتى تتحرك النافذة معه:
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’ بتسمية عطلة نهاية الأسبوع خطوة أولى شائعة حين يسأل التسويق هل تستحق حملات عطلة نهاية الأسبوع العناء:
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 لتحصل على الساعات. هذه تذاكر ديسمبر العاجلة مع أوقات حلّها:
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 نفسه الإجابة نفسها غدًا.