MySQL وتصميم قواعد البيانات

التعبيرات الجدولية الشائعة (CTEs)

13 دقيقة الدرس 11 من 40

التعبيرات الجدولية الشائعة (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:

  1. البحث عن العملاء الذين اشتروا في 3 أشهر متتالية (استخدم CTE مع دالة النافذة)
  2. أنشئ CTE متكررة لإظهار تسلسل فئة المنتج الهرمي
  3. أنشئ تقريراً يعرض الإيرادات الشهرية مع متوسط متحرك لمدة 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 مقابل الاستعلامات الفرعية مقابل الجداول المؤقتة
  • اعتبارات الأداء وأفضل الممارسات
التالي: في الدرس التالي، سنستكشف دوال السلسلة النصية والتاريخ المتقدمة لمعالجة وتحويل البيانات القوية!

ES
Edrees Salih
منذ 19 ساعة

We are still cooking the magic in the way!