We are still cooking the magic in the way!
MySQL وتصميم قواعد البيانات
التعبيرات الجدولية الشائعة (CTEs)
التعبيرات الجدولية الشائعة (CTEs)
التعبيرات الجدولية الشائعة (CTEs) هي مجموعات نتائج مؤقتة مسماة موجودة فقط أثناء تنفيذ الاستعلام. تجعل الاستعلامات المعقدة أكثر قابلية للقراءة والصيانة ويمكنها التعامل مع هياكل البيانات المتكررة التي قد تكون صعبة أو مستحيلة مع الاستعلامات التقليدية.
ما هي CTEs؟
يتم تعريف CTE باستخدام جملة WITH ويمكن الإشارة إليها عدة مرات في الاستعلام الرئيسي:
-- بناء جملة CTE الأساسي
WITH cte_name AS (
SELECT column1, column2
FROM table_name
WHERE condition
)
SELECT *
FROM cte_name;
الفوائد الرئيسية: تحسن CTEs القابلية للقراءة من خلال تقسيم الاستعلامات المعقدة إلى خطوات منطقية، وتسمح بإعادة استخدام نفس الاستعلام الفرعي عدة مرات، وتمكّن الاستعلامات المتكررة للبيانات الهرمية.
CTEs البسيطة
تستبدل CTEs البسيطة الاستعلامات الفرعية المعقدة بمجموعات نتائج مسماة قابلة لإعادة الاستخدام:
-- بدون CTE (صعب القراءة)
SELECT c.customer_name, o.total_spent
FROM customers c
JOIN (
SELECT customer_id, SUM(total_amount) AS total_spent
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
) o ON c.customer_id = o.customer_id
WHERE o.total_spent > 1000;
-- مع CTE (أوضح بكثير)
WITH customer_spending AS (
SELECT customer_id, SUM(total_amount) AS total_spent
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
)
SELECT c.customer_name, cs.total_spent
FROM customers c
JOIN customer_spending cs ON c.customer_id = cs.customer_id
WHERE cs.total_spent > 1000;
-- حساب الإحصائيات باستخدام CTE
WITH product_stats AS (
SELECT
category,
AVG(price) AS avg_price,
COUNT(*) AS product_count,
MAX(price) AS max_price
FROM products
GROUP BY category
)
SELECT
p.product_name,
p.category,
p.price,
ps.avg_price,
ROUND((p.price - ps.avg_price) / ps.avg_price * 100, 2) AS pct_diff_from_avg
FROM products p
JOIN product_stats ps ON p.category = ps.category
WHERE p.price > ps.avg_price
ORDER BY pct_diff_from_avg DESC;
CTEs متعددة
يمكنك تعريف CTEs متعددة في استعلام واحد، مفصولة بفواصل:
-- CTEs متعددة للتحليل المعقد
WITH
high_value_customers AS (
SELECT customer_id, SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING total_spent > 5000
),
recent_orders AS (
SELECT customer_id, COUNT(*) AS recent_order_count
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)
GROUP BY customer_id
),
product_diversity AS (
SELECT o.customer_id, COUNT(DISTINCT oi.product_id) AS unique_products
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY o.customer_id
)
SELECT
c.customer_name,
c.email,
hvc.total_spent,
COALESCE(ro.recent_order_count, 0) AS recent_orders,
COALESCE(pd.unique_products, 0) AS products_purchased
FROM customers c
JOIN high_value_customers hvc ON c.customer_id = hvc.customer_id
LEFT JOIN recent_orders ro ON c.customer_id = ro.customer_id
LEFT JOIN product_diversity pd ON c.customer_id = pd.customer_id
ORDER BY hvc.total_spent DESC;
أفضل ممارسة: استخدم أسماء CTE وصفية تشير بوضوح إلى البيانات التي تحتوي عليها. هذا يجعل الاستعلامات موثقة ذاتياً وأسهل في الصيانة.
CTEs التي تشير إلى CTEs أخرى
يمكن لـ CTEs اللاحقة الإشارة إلى CTEs السابقة في نفس جملة WITH:
-- CTEs تبني على بعضها البعض
WITH
monthly_sales AS (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(total_amount) AS revenue
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
),
sales_with_growth AS (
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue
FROM monthly_sales
),
growth_analysis AS (
SELECT
month,
revenue,
prev_month_revenue,
revenue - prev_month_revenue AS growth_amount,
ROUND((revenue - prev_month_revenue) * 100.0 / prev_month_revenue, 2) AS growth_pct
FROM sales_with_growth
WHERE prev_month_revenue IS NOT NULL
)
SELECT *
FROM growth_analysis
WHERE ABS(growth_pct) > 10
ORDER BY month;
CTEs المتكررة
يمكن لـ CTEs المتكررة الإشارة إلى نفسها، مثالية للبيانات الهرمية أو بنية الشجرة:
-- بناء جملة CTE المتكررة الأساسية
WITH RECURSIVE cte_name AS (
-- العضو الأساسي (الحالة الأساسية)
SELECT columns
FROM table
WHERE base_condition
UNION ALL
-- العضو المتكرر
SELECT columns
FROM table
JOIN cte_name ON join_condition
WHERE recursive_condition
)
SELECT * FROM cte_name;
-- تسلسل الموظفين الهرمي (المخطط التنظيمي)
WITH RECURSIVE employee_hierarchy AS (
-- الأساس: ابدأ بالرئيس التنفيذي (بدون مدير)
SELECT
employee_id,
employee_name,
manager_id,
position,
1 AS level,
CAST(employee_name AS CHAR(500)) AS hierarchy_path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- متكرر: أضف الموظفين الذين يتبعون المستوى السابق
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
e.position,
eh.level + 1,
CONCAT(eh.hierarchy_path, ' > ', e.employee_name)
FROM employees e
JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
WHERE eh.level < 10 -- منع الحلقات اللانهائية
)
SELECT
REPEAT(' ', level - 1) AS indent,
employee_name,
position,
level,
hierarchy_path
FROM employee_hierarchy
ORDER BY hierarchy_path;
مهم: قم دائماً بتضمين شرط إنهاء في CTEs المتكررة لمنع الحلقات اللانهائية. استخدم عداد المستوى أو حدد العمق باستخدام جملة WHERE.
أمثلة متكررة من العالم الحقيقي
-- شجرة الفئات مع جميع الأسلاف
WITH RECURSIVE category_tree AS (
-- الأساس: الفئات الورقية
SELECT
category_id,
category_name,
parent_category_id,
1 AS depth,
category_name AS full_path
FROM categories
WHERE category_id = 42 -- ابدأ من فئة محددة
UNION ALL
-- متكرر: أضف الفئات الأصلية
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
ct.depth + 1,
CONCAT(c.category_name, ' > ', ct.full_path)
FROM categories c
JOIN category_tree ct ON c.category_id = ct.parent_category_id
)
SELECT * FROM category_tree
ORDER BY depth DESC;
-- البحث عن جميع المرؤوسين لمدير
WITH RECURSIVE subordinates AS (
-- الأساس: المدير
SELECT employee_id, employee_name, manager_id, 0 AS levels_down
FROM employees
WHERE employee_id = 5 -- معرف المدير
UNION ALL
-- متكرر: تقاريرهم
SELECT e.employee_id, e.employee_name, e.manager_id, s.levels_down + 1
FROM employees e
JOIN subordinates s ON e.manager_id = s.employee_id
WHERE s.levels_down < 5
)
SELECT
levels_down,
employee_name,
(SELECT COUNT(*) - 1 FROM subordinates) AS total_subordinates
FROM subordinates
ORDER BY levels_down, employee_name;
توليد التسلسلات باستخدام CTEs المتكررة
-- توليد نطاق التاريخ
WITH RECURSIVE date_range AS (
SELECT DATE('2024-01-01') AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM date_range
WHERE date < '2024-12-31'
)
SELECT
dr.date,
COALESCE(SUM(o.total_amount), 0) AS daily_revenue
FROM date_range dr
LEFT JOIN orders o ON DATE(o.order_date) = dr.date
GROUP BY dr.date
ORDER BY dr.date;
-- توليد تسلسل الأرقام (1 إلى 100)
WITH RECURSIVE numbers AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1
FROM numbers
WHERE n < 100
)
SELECT n FROM numbers;
CTEs مقابل الاستعلامات الفرعية مقابل الجداول المؤقتة
متى تستخدم كل واحدة:
- CTEs: تحسين القابلية للقراءة، الحاجة للإشارة إلى النتيجة عدة مرات، استعلامات متكررة
- الاستعلامات الفرعية: استخدام لمرة واحدة بسيط، مجموعات نتائج صغيرة
- الجداول المؤقتة: مجموعات نتائج كبيرة جداً، الحاجة إلى فهارس، استعلامات متعددة تستخدم نفس البيانات
-- أداء CTE مقابل الاستعلام الفرعي
-- CTE (يمكن الإشارة إليها عدة مرات)
WITH top_products AS (
SELECT product_id, product_name, sales_count
FROM products
WHERE sales_count > 100
)
SELECT * FROM top_products WHERE product_name LIKE 'A%'
UNION ALL
SELECT * FROM top_products WHERE product_name LIKE 'B%';
-- استعلام فرعي (يُحسب مرتين - أقل كفاءة)
SELECT * FROM (SELECT ...) WHERE product_name LIKE 'A%'
UNION ALL
SELECT * FROM (SELECT ...) WHERE product_name LIKE 'B%';
التأثيرات على الأداء
نصائح الأداء:
- يتم تجسيد CTEs عادةً مرة واحدة وإعادة استخدامها (MySQL 8.0+)
- أضف فهارس إلى الجداول الأساسية، وليس CTEs (إنها مؤقتة)
- لمجموعات النتائج الكبيرة جداً، فكر في الجداول المؤقتة مع الفهارس
- يمكن أن تكون CTEs المتكررة بطيئة - قم دائماً بتحديد عمق التكرار
- استخدم EXPLAIN لفهم كيفية تنفيذ MySQL لاستعلامات CTE
مثال معقد من العالم الحقيقي
-- تحليل الاحتفاظ بمجموعة العملاء
WITH RECURSIVE
months AS (
SELECT DATE_FORMAT(MIN(order_date), '%Y-%m-01') AS month
FROM orders
UNION ALL
SELECT DATE_ADD(month, INTERVAL 1 MONTH)
FROM months
WHERE month < DATE_FORMAT(CURDATE(), '%Y-%m-01')
),
first_purchases AS (
SELECT
customer_id,
DATE_FORMAT(MIN(order_date), '%Y-%m-01') AS cohort_month
FROM orders
GROUP BY customer_id
),
customer_months AS (
SELECT
fp.customer_id,
fp.cohort_month,
DATE_FORMAT(o.order_date, '%Y-%m-01') AS purchase_month,
TIMESTAMPDIFF(MONTH, fp.cohort_month, DATE_FORMAT(o.order_date, '%Y-%m-01')) AS months_since_first
FROM first_purchases fp
JOIN orders o ON fp.customer_id = o.customer_id
)
SELECT
cohort_month,
COUNT(DISTINCT customer_id) AS cohort_size,
SUM(CASE WHEN months_since_first = 0 THEN 1 ELSE 0 END) AS month_0,
SUM(CASE WHEN months_since_first = 1 THEN 1 ELSE 0 END) AS month_1,
SUM(CASE WHEN months_since_first = 2 THEN 1 ELSE 0 END) AS month_2,
SUM(CASE WHEN months_since_first = 3 THEN 1 ELSE 0 END) AS month_3,
ROUND(SUM(CASE WHEN months_since_first = 3 THEN 1 ELSE 0 END) * 100.0 /
COUNT(DISTINCT customer_id), 2) AS retention_3month_pct
FROM customer_months
GROUP BY cohort_month
ORDER BY cohort_month;
تمرين عملي:
التحدي: اكتب استعلامات باستخدام CTEs:
- البحث عن العملاء الذين اشتروا في 3 أشهر متتالية (استخدم CTE مع دالة النافذة)
- أنشئ CTE متكررة لإظهار تسلسل فئة المنتج الهرمي
- أنشئ تقريراً يعرض الإيرادات الشهرية مع متوسط متحرك لمدة 3 أشهر (CTEs متعددة)
تلميحات الحل:
-- 1. المشتريات المتتالية
WITH monthly_purchases AS (
SELECT DISTINCT
customer_id,
DATE_FORMAT(order_date, '%Y-%m-01') AS month
FROM orders
),
with_sequence AS (
SELECT
customer_id,
month,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY month) AS rn,
DATE_SUB(month, INTERVAL ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY month) MONTH) AS grp
FROM monthly_purchases
)
SELECT customer_id, MIN(month) AS start_month, COUNT(*) AS consecutive_months
FROM with_sequence
GROUP BY customer_id, grp
HAVING consecutive_months >= 3;
-- 2. تسلسل الفئة الهرمي
WITH RECURSIVE category_path AS (
SELECT category_id, category_name, parent_id, 1 AS level,
category_name AS path
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.category_id, c.category_name, c.parent_id, cp.level + 1,
CONCAT(cp.path, ' > ', c.category_name)
FROM categories c
JOIN category_path cp ON c.parent_id = cp.category_id
)
SELECT * FROM category_path ORDER BY path;
الملخص
في هذا الدرس، أتقنت:
- بناء جملة CTE مع جملة WITH لمجموعات النتائج المسماة
- CTEs متعددة و CTEs تشير إلى بعضها البعض
- CTEs المتكررة لهياكل البيانات الهرمية
- التطبيقات العملية: المخططات التنظيمية، أشجار الفئات، نطاقات التاريخ
- متى تستخدم CTEs مقابل الاستعلامات الفرعية مقابل الجداول المؤقتة
- اعتبارات الأداء وأفضل الممارسات
التالي: في الدرس التالي، سنستكشف دوال السلسلة النصية والتاريخ المتقدمة لمعالجة وتحويل البيانات القوية!