الدرس 33
دوال SQL
دوال التاريخ والوقت DateTime Functions
التعامل مع التواريخ والأوقات من أهم المهارات في SQL. سنتعلم كيفية الحصول على التاريخ الحالي، استخراج أجزاء التاريخ، حساب الفروق، وتنسيق التواريخ للتقارير.
أنواع بيانات التاريخ والوقت
📅 الأنواع الرئيسية
| النوع | الصيغة | المثال | الاستخدام |
|---|---|---|---|
| DATE | YYYY-MM-DD | 2025-01-15 | التاريخ فقط |
| TIME | HH:MM:SS | 14:30:00 | الوقت فقط |
| DATETIME | YYYY-MM-DD HH:MM:SS | 2025-01-15 14:30:00 | التاريخ والوقت |
| TIMESTAMP | YYYY-MM-DD HH:MM:SS | 2025-01-15 14:30:00 | التاريخ والوقت مع المنطقة |
| YEAR | YYYY | 2025 | السنة فقط |
الحصول على التاريخ والوقت الحالي
SQL - التاريخ والوقت الحالي
-- MySQL
SELECT NOW(); -- 2025-01-15 14:30:45
SELECT CURRENT_TIMESTAMP; -- 2025-01-15 14:30:45
SELECT CURRENT_TIMESTAMP();-- 2025-01-15 14:30:45
SELECT CURDATE(); -- 2025-01-15
SELECT CURRENT_DATE; -- 2025-01-15
SELECT CURTIME(); -- 14:30:45
SELECT CURRENT_TIME; -- 14:30:45
-- PostgreSQL
SELECT NOW(); -- 2025-01-15 14:30:45.123456+03
SELECT CURRENT_TIMESTAMP; -- 2025-01-15 14:30:45.123456+03
SELECT CURRENT_DATE; -- 2025-01-15
SELECT CURRENT_TIME; -- 14:30:45.123456+03
-- SQL Server
SELECT GETDATE(); -- 2025-01-15 14:30:45.123
SELECT SYSDATETIME(); -- 2025-01-15 14:30:45.1234567
SELECT GETUTCDATE(); -- 2025-01-15 11:30:45.123 (UTC)
-- Oracle
SELECT SYSDATE FROM dual; -- 15-JAN-25
SELECT SYSTIMESTAMP FROM dual;
استخدامات عملية
SQL - سجلات اليوم
-- طلبات اليوم
SELECT * FROM orders
WHERE DATE(order_date) = CURDATE();
-- طلبات الأسبوع الحالي
SELECT * FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY);
-- إدخال سجل جديد بتاريخ حالي
INSERT INTO logs (action, created_at)
VALUES ('تسجيل دخول', NOW());
-- تحديث تاريخ آخر تعديل
UPDATE products
SET updated_at = NOW()
WHERE product_id = 1;
استخراج أجزاء التاريخ
SQL - استخراج السنة والشهر واليوم
-- MySQL - دوال مخصصة
SELECT
order_date,
YEAR(order_date) AS year,
MONTH(order_date) AS month,
DAY(order_date) AS day,
HOUR(order_date) AS hour,
MINUTE(order_date) AS minute,
SECOND(order_date) AS second
FROM orders;
-- EXTRACT (معيارية - تعمل في معظم القواعد)
SELECT
order_date,
EXTRACT(YEAR FROM order_date) AS year,
EXTRACT(MONTH FROM order_date) AS month,
EXTRACT(DAY FROM order_date) AS day,
EXTRACT(HOUR FROM order_date) AS hour,
EXTRACT(MINUTE FROM order_date) AS minute,
EXTRACT(SECOND FROM order_date) AS second
FROM orders;
استخراج معلومات إضافية
SQL - معلومات متقدمة
-- MySQL
SELECT
order_date,
DAYOFWEEK(order_date) AS day_of_week, -- 1=الأحد, 7=السبت
WEEKDAY(order_date) AS weekday, -- 0=الاثنين, 6=الأحد
DAYOFYEAR(order_date) AS day_of_year, -- 1-366
WEEK(order_date) AS week_number, -- رقم الأسبوع
QUARTER(order_date) AS quarter, -- الربع (1-4)
DAYNAME(order_date) AS day_name, -- Sunday, Monday...
MONTHNAME(order_date) AS month_name -- January, February...
FROM orders;
-- PostgreSQL
SELECT
order_date,
EXTRACT(DOW FROM order_date) AS day_of_week, -- 0=الأحد
EXTRACT(DOY FROM order_date) AS day_of_year,
EXTRACT(WEEK FROM order_date) AS week_number,
EXTRACT(QUARTER FROM order_date) AS quarter,
TO_CHAR(order_date, 'Day') AS day_name,
TO_CHAR(order_date, 'Month') AS month_name
FROM orders;
تقارير حسب الفترات
SQL - تجميع حسب الفترات
-- مبيعات حسب الشهر
SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
SUM(total_amount) AS monthly_sales
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY year, month;
-- مبيعات حسب الربع
SELECT
YEAR(order_date) AS year,
QUARTER(order_date) AS quarter,
SUM(total_amount) AS quarterly_sales
FROM orders
GROUP BY YEAR(order_date), QUARTER(order_date);
-- مبيعات حسب يوم الأسبوع
SELECT
DAYNAME(order_date) AS day_name,
COUNT(*) AS order_count,
SUM(total_amount) AS daily_sales
FROM orders
GROUP BY DAYOFWEEK(order_date), DAYNAME(order_date)
ORDER BY DAYOFWEEK(order_date);
العمليات الحسابية على التواريخ
إضافة وطرح فترات
SQL - DATE_ADD و DATE_SUB
-- MySQL: DATE_ADD / DATE_SUB
SELECT
order_date,
DATE_ADD(order_date, INTERVAL 7 DAY) AS delivery_date,
DATE_ADD(order_date, INTERVAL 1 MONTH) AS next_month,
DATE_ADD(order_date, INTERVAL 1 YEAR) AS next_year,
DATE_SUB(order_date, INTERVAL 30 DAY) AS thirty_days_ago
FROM orders;
-- وحدات INTERVAL المتاحة:
-- SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR
-- SECOND_MICROSECOND, MINUTE_MICROSECOND, MINUTE_SECOND
-- HOUR_MICROSECOND, HOUR_SECOND, HOUR_MINUTE
-- DAY_MICROSECOND, DAY_SECOND, DAY_MINUTE, DAY_HOUR
-- YEAR_MONTH
-- أمثلة إضافية
SELECT DATE_ADD(NOW(), INTERVAL 2 HOUR); -- بعد ساعتين
SELECT DATE_ADD(NOW(), INTERVAL '1:30' HOUR_MINUTE); -- بعد ساعة ونصف
SELECT DATE_ADD(NOW(), INTERVAL '1-6' YEAR_MONTH); -- بعد سنة و 6 أشهر
حساب الفرق بين تاريخين
SQL - DATEDIFF و TIMESTAMPDIFF
-- MySQL: DATEDIFF (الفرق بالأيام)
SELECT DATEDIFF('2025-12-31', '2025-01-01'); -- 364
-- TIMESTAMPDIFF (الفرق بوحدة محددة)
SELECT
order_date,
ship_date,
TIMESTAMPDIFF(DAY, order_date, ship_date) AS days_to_ship,
TIMESTAMPDIFF(HOUR, order_date, ship_date) AS hours_to_ship
FROM orders;
-- حساب عمر العميل
SELECT
customer_name,
birth_date,
TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age
FROM customers;
-- PostgreSQL: طرح مباشر
SELECT
ship_date - order_date AS days_diff,
AGE(ship_date, order_date) AS age_diff
FROM orders;
-- SQL Server: DATEDIFF
SELECT DATEDIFF(day, order_date, ship_date) AS days_diff
FROM orders;
أمثلة عملية
SQL - سيناريوهات حقيقية
-- تاريخ انتهاء الاشتراك (بعد سنة)
SELECT
user_id,
subscription_date,
DATE_ADD(subscription_date, INTERVAL 1 YEAR) AS expiry_date
FROM subscriptions;
-- الاشتراكات المنتهية قريباً (خلال 30 يوم)
SELECT * FROM subscriptions
WHERE DATE_ADD(subscription_date, INTERVAL 1 YEAR)
BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY);
-- متوسط وقت التوصيل
SELECT
AVG(DATEDIFF(ship_date, order_date)) AS avg_delivery_days
FROM orders
WHERE ship_date IS NOT NULL;
-- الطلبات المتأخرة (أكثر من 5 أيام)
SELECT *
FROM orders
WHERE DATEDIFF(ship_date, order_date) > 5
OR (ship_date IS NULL AND DATEDIFF(CURDATE(), order_date) > 5);
تنسيق التواريخ
SQL - DATE_FORMAT (MySQL)
-- MySQL: DATE_FORMAT
SELECT
order_date,
DATE_FORMAT(order_date, '%Y-%m-%d') AS iso_date,
DATE_FORMAT(order_date, '%d/%m/%Y') AS arabic_date,
DATE_FORMAT(order_date, '%W, %M %d, %Y') AS full_date,
DATE_FORMAT(order_date, '%H:%i:%s') AS time_only,
DATE_FORMAT(order_date, '%Y-%m') AS year_month
FROM orders;
-- رموز التنسيق الشائعة:
-- %Y = السنة (4 أرقام) %y = السنة (رقمين)
-- %M = اسم الشهر كامل %m = رقم الشهر (01-12)
-- %b = اسم الشهر مختصر %c = رقم الشهر (1-12)
-- %d = اليوم (01-31) %e = اليوم (1-31)
-- %W = اسم اليوم كامل %a = اسم اليوم مختصر
-- %H = الساعة (00-23) %h = الساعة (01-12)
-- %i = الدقائق (00-59) %s = الثواني (00-59)
-- %p = AM/PM
تنسيقات لقواعد بيانات أخرى
SQL - TO_CHAR و FORMAT
-- PostgreSQL: TO_CHAR
SELECT
order_date,
TO_CHAR(order_date, 'YYYY-MM-DD') AS iso_date,
TO_CHAR(order_date, 'DD/MM/YYYY') AS arabic_date,
TO_CHAR(order_date, 'Day, Month DD, YYYY') AS full_date,
TO_CHAR(order_date, 'HH24:MI:SS') AS time_only
FROM orders;
-- SQL Server: FORMAT
SELECT
order_date,
FORMAT(order_date, 'yyyy-MM-dd') AS iso_date,
FORMAT(order_date, 'dd/MM/yyyy') AS arabic_date,
FORMAT(order_date, 'dddd, MMMM dd, yyyy') AS full_date
FROM orders;
-- Oracle: TO_CHAR
SELECT
order_date,
TO_CHAR(order_date, 'YYYY-MM-DD') AS iso_date,
TO_CHAR(order_date, 'DD/MM/YYYY') AS arabic_date
FROM orders;
المناطق الزمنية
SQL - التعامل مع المناطق الزمنية
-- MySQL: تحويل المنطقة الزمنية
SELECT
NOW() AS server_time,
CONVERT_TZ(NOW(), 'UTC', 'Asia/Riyadh') AS riyadh_time,
CONVERT_TZ(NOW(), 'UTC', 'America/New_York') AS new_york_time;
-- عرض المنطقة الزمنية الحالية
SELECT @@global.time_zone, @@session.time_zone;
-- PostgreSQL
SELECT
NOW() AS server_time,
NOW() AT TIME ZONE 'UTC' AS utc_time,
NOW() AT TIME ZONE 'Asia/Riyadh' AS riyadh_time;
-- تخزين التوقيت بـ UTC وعرضه محلياً
SELECT
created_at,
CONVERT_TZ(created_at, 'UTC', 'Asia/Riyadh') AS local_time
FROM events;
💡 أفضل الممارسات للمناطق الزمنية
- خزّن التواريخ دائماً بتوقيت UTC
- حوّل للتوقيت المحلي عند العرض فقط
- استخدم TIMESTAMP مع المنطقة الزمنية عند الحاجة
- تجنب تخزين التاريخ كنص
أمثلة عملية شاملة
1. تقرير المبيعات اليومية
SQL - تقرير يومي
SELECT
DATE(order_date) AS date,
DAYNAME(order_date) AS day_name,
COUNT(*) AS total_orders,
SUM(total_amount) AS total_sales,
ROUND(AVG(total_amount), 2) AS avg_order_value
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(order_date), DAYNAME(order_date)
ORDER BY date DESC;
2. مقارنة الفترات
SQL - هذا الشهر vs الشهر الماضي
SELECT
'هذا الشهر' AS period,
COUNT(*) AS orders,
SUM(total_amount) AS sales
FROM orders
WHERE YEAR(order_date) = YEAR(CURDATE())
AND MONTH(order_date) = MONTH(CURDATE())
UNION ALL
SELECT
'الشهر الماضي' AS period,
COUNT(*) AS orders,
SUM(total_amount) AS sales
FROM orders
WHERE order_date >= DATE_SUB(
DATE_FORMAT(CURDATE(), '%Y-%m-01'),
INTERVAL 1 MONTH
)
AND order_date < DATE_FORMAT(CURDATE(), '%Y-%m-01');
3. تحليل سلوك العملاء
SQL - آخر نشاط للعملاء
SELECT
c.customer_id,
c.customer_name,
MAX(o.order_date) AS last_order_date,
DATEDIFF(CURDATE(), MAX(o.order_date)) AS days_since_last_order,
CASE
WHEN MAX(o.order_date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 'نشط'
WHEN MAX(o.order_date) >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) THEN 'متوسط'
WHEN MAX(o.order_date) >= DATE_SUB(CURDATE(), INTERVAL 180 DAY) THEN 'خامل'
ELSE 'مفقود'
END AS customer_status
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name;
4. تقويم المبيعات
SQL - إنشاء تقويم كامل
-- إنشاء جدول تقويم مؤقت
WITH RECURSIVE calendar AS (
SELECT DATE('2025-01-01') AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM calendar
WHERE date < '2025-12-31'
)
SELECT
cal.date,
DAYNAME(cal.date) AS day_name,
COALESCE(COUNT(o.order_id), 0) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS daily_sales
FROM calendar cal
LEFT JOIN orders o ON DATE(o.order_date) = cal.date
GROUP BY cal.date
ORDER BY cal.date;
5. ساعات الذروة
SQL - تحليل ساعات الذروة
SELECT
HOUR(order_date) AS hour,
COUNT(*) AS order_count,
SUM(total_amount) AS hourly_sales,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1) AS percentage
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY HOUR(order_date)
ORDER BY hour;
6. الاشتراكات والتجديد
SQL - إدارة الاشتراكات
SELECT
user_id,
plan_name,
start_date,
DATE_ADD(start_date, INTERVAL duration_months MONTH) AS end_date,
DATEDIFF(
DATE_ADD(start_date, INTERVAL duration_months MONTH),
CURDATE()
) AS days_remaining,
CASE
WHEN DATE_ADD(start_date, INTERVAL duration_months MONTH) < CURDATE()
THEN 'منتهي'
WHEN DATE_ADD(start_date, INTERVAL duration_months MONTH)
<= DATE_ADD(CURDATE(), INTERVAL 7 DAY)
THEN 'ينتهي قريباً'
ELSE 'نشط'
END AS status
FROM subscriptions;
التحقق من التواريخ
SQL - التحقق من صحة التاريخ
-- MySQL: التحقق من تاريخ صحيح
SELECT
date_string,
STR_TO_DATE(date_string, '%Y-%m-%d') AS parsed_date,
CASE
WHEN STR_TO_DATE(date_string, '%Y-%m-%d') IS NULL
THEN 'تاريخ غير صحيح'
ELSE 'تاريخ صحيح'
END AS validation
FROM raw_data;
-- البحث عن تواريخ مستقبلية غير منطقية
SELECT * FROM orders
WHERE order_date > NOW();
-- البحث عن تواريخ ميلاد غير منطقية
SELECT * FROM customers
WHERE birth_date > CURDATE()
OR TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) > 120;
-- التحقق من تواريخ NULL
SELECT
COUNT(*) AS total,
SUM(CASE WHEN order_date IS NULL THEN 1 ELSE 0 END) AS null_dates,
SUM(CASE WHEN ship_date IS NULL THEN 1 ELSE 0 END) AS null_ship_dates
FROM orders;