تنظيم الاستعلامات باستخدام تعبيرات الجداول المشتركة

مكتمل

عند العمل مع استعلامات معقدة، غالبا ما تحتاج إلى تقسيم المنطق إلى أجزاء قابلة للإدارة أو الإشارة إلى نفس الاستعلام الفرعي عدة مرات. توفر التعبيرات الشائعة للجداول (CTEs) طريقة لتعريف مجموعات نتائج مؤقتة مسماة توجد فقط أثناء استعلام واحد، مما يجعل كودك أكثر قابلية للقراءة وسهولة في الصيانة.

فهم بناء جملة CTE

يتم تعريف تعبير الجدول المشترك باستخدام الجملة WITH ، يليه اسم CTE، وقائمة أعمدة اختيارية، واستعلام يحدد مجموعة النتائج. يمكن بعد ذلك الإشارة إلى CTE في العبارة التالية SELECT، INSERT، ، UPDATEأو DELETE الجملة.

WITH CTE_Name (Column1, Column2)
AS
(
    -- CTE query definition
    SELECT Column1, Column2
    FROM SomeTable
    WHERE SomeCondition = 'Value'
)
SELECT * FROM CTE_Name;

تقدم CTEs عدة مزايا مقارنة بالجداول المشتقة والاستعلامات الفرعية:

  • تحسين قابلية القراءة: تصبح الاستعلامات المعقدة أسهل في الفهم عند تقسيمها إلى أقسام منطقية مسماة
  • القدرة على الإشارة الذاتية: يمكن لتقنيات CTE التكرارية الإشارة إلى نفسها، مما يمكن من التنقل الهرمي للبيانات
  • المراجع المتعددة: الإشارة إلى نفس CTE عدة مرات في الاستعلام الخارجي دون إعادة تعريفه
  • التصميم المعياري: بناء استعلامات معقدة تدريجيا عن طريق تعريف عدة CTEs

‏‫ملاحظة‬

CTEs هي مجموعات نتائج مؤقتة توجد فقط أثناء تنفيذ الاستعلام. على عكس الجداول التقليدية، فهي لا تستمر بعد العبارة التي تستخدمها ولا تتطلب تنظيفا صريحا.

إنشاء CTEs غير تكرارية

تعرف CTEs غير التكرارية مجموعة نتائج بناء على استعلام مباشر لا يشير إلى نفسه. هذا النمط مفيد لتبسيط الروابط المعقدة، أو تفكيك الحسابات متعددة الخطوات، أو تحسين تنظيم الكود.

المثال التالي يستخدم CTE لحساب مقاييس المبيعات قبل الانضمام إلى معلومات المنتج:

WITH SalesSummary AS
(
    SELECT 
        ProductID,
        SUM(OrderQty) AS TotalQuantity,
        SUM(LineTotal) AS TotalRevenue,
        COUNT(DISTINCT SalesOrderID) AS OrderCount
    FROM SalesLT.SalesOrderDetail
    GROUP BY ProductID
)
SELECT 
    p.Name AS ProductName,
    p.ProductNumber,
    p.ListPrice,
    ss.TotalQuantity,
    ss.TotalRevenue,
    ss.OrderCount,
    ss.TotalRevenue / NULLIF(ss.TotalQuantity, 0) AS AverageUnitPrice
FROM SalesLT.Product AS p
INNER JOIN SalesSummary AS ss
    ON p.ProductID = ss.ProductID
ORDER BY ss.TotalRevenue DESC;

يقوم هذا الاستعلام أولا بإنشاء CTE يسمى SalesSummary يجمع تفاصيل الطلب حسب المنتج، ويحسب إجمالي الكمية المباعة، إجمالي الإيرادات، وعدد الطلبات. ثم يربط الاستعلام الرئيسي هذا النموذج الاستهلاكي بالجدول Product لعرض أسماء المنتجات إلى جانب مؤشرات المبيعات. تمنع الدالة NULLIF القسمة على الصفر عند حساب متوسط سعر الوحدة.

يمكنك تعريف عدة CTEs في جملة واحدة WITH عن طريق فصلها بفواصل. يمكن لتقنيات CTE اللاحقة الإشارة إلى النماذج السابقة، مما يمكن التحويل التدريجي للبيانات:

WITH CategorySales AS
(
    SELECT 
        p.ProductCategoryID,
        SUM(sod.LineTotal) AS CategoryRevenue
    FROM SalesLT.Product AS p
    INNER JOIN SalesLT.SalesOrderDetail AS sod
        ON p.ProductID = sod.ProductID
    GROUP BY p.ProductCategoryID
),
RankedCategories AS
(
    SELECT 
        ProductCategoryID,
        CategoryRevenue,
        RANK() OVER (ORDER BY CategoryRevenue DESC) AS RevenueRank
    FROM CategorySales
)
SELECT 
    pc.Name AS CategoryName,
    rc.CategoryRevenue,
    rc.RevenueRank
FROM RankedCategories AS rc
INNER JOIN SalesLT.ProductCategory AS pc
    ON rc.ProductCategoryID = pc.ProductCategoryID
WHERE rc.RevenueRank <= 5;

نصيحة

عند بناء استعلامات تحتوي على عدة CTEs، ابدأ بأكثر تحويلات البيانات تفصيلا وقم بتجميع أو تصفية تدريجيا في CTEs التالية. هذا النهج يجعل التصحيح أسهل لأنك تستطيع اختبار كل CTE بشكل مستقل.

إنشاء CTEs تكرارية

تشير CTEs التكرارية إلى هياكل بيانات هرمية أو شبيهة بالرسوم البيانية للعمليات الهرمية. يتكون CTE التكراري من جزأين: عضو مرساة يوفر مجموعة النتائج الأولية، وعضو تكراري يشير إلى CTE للبناء على النتائج السابقة.

النحوية العامة لتعديل CTE التكراري هي:

WITH RecursiveCTE AS
(
    -- Anchor member: starting point
    SELECT columns
    FROM table
    WHERE starting_condition
    
    UNION ALL
    
    -- Recursive member: references the CTE
    SELECT columns
    FROM table
    INNER JOIN RecursiveCTE
        ON join_condition
)
SELECT * FROM RecursiveCTE;

حالة استخدام شائعة هي التنقل في التسلسل الهرمي التنظيمي. خذ جدول موظف حيث لكل موظف مدير:

WITH EmployeeHierarchy AS
(
    -- Anchor: Start with top-level managers (no manager)
    SELECT 
        EmployeeID,
        FirstName,
        LastName,
        ManagerID,
        0 AS Level,
        CAST(FirstName + ' ' + LastName AS NVARCHAR(500)) AS HierarchyPath
    FROM HumanResources.Employee
    WHERE ManagerID IS NULL
    
    UNION ALL
    
    -- Recursive: Find employees who report to previously found employees
    SELECT 
        e.EmployeeID,
        e.FirstName,
        e.LastName,
        e.ManagerID,
        eh.Level + 1,
        CAST(eh.HierarchyPath + ' > ' + e.FirstName + ' ' + e.LastName AS NVARCHAR(500))
    FROM HumanResources.Employee AS e
    INNER JOIN EmployeeHierarchy AS eh
        ON e.ManagerID = eh.EmployeeID
)
SELECT 
    EmployeeID,
    FirstName,
    LastName,
    Level,
    HierarchyPath
FROM EmployeeHierarchy
ORDER BY HierarchyPath;

مهم

يمكن أن تسبب CTEs العودية حلقات لا نهائية إذا لم يتم تحقيق شرط الإنهاء أبدا. يحد SQL Server من التكرار إلى 100 مستوى بشكل افتراضي. يستخدم OPTION (MAXRECURSION n) لتغيير هذا الحد، حيث n هو أقصى عمق عودية (0 للغير محدود).

توليد تسلسلات باستخدام CTEs تكرارية

تتفوق CTEs العودية في توليد تسلسلات من الأعداد أو التواريخ دون الحاجة إلى جدول أرقام فعلي. هذه التقنية مفيدة لإنشاء نطاقات تواريخ، أو سد الفجوات في البيانات، أو توليد بيانات الاختبار:

-- Generate a sequence of dates for the current month
WITH DateSequence AS
(
    -- Anchor: Get the first day of the current month
    SELECT CAST(DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) AS DATE) AS DateValue
    
    UNION ALL
    
    -- Recursive: Add one day until we reach the end of the month
    SELECT DATEADD(DAY, 1, DateValue)
    FROM DateSequence
    WHERE DateValue < EOMONTH(GETDATE())
)
-- Output each date with its day name
SELECT DateValue, DATENAME(WEEKDAY, DateValue) AS DayName
FROM DateSequence
OPTION (MAXRECURSION 31);  -- Allow up to 31 iterations (max days in a month)

يمكنك دمج التسلسلات المولدة مع البيانات الفعلية لتحديد الفجوات أو إنشاء تقارير ملخصة:

-- Generate a numbers table from 1 to 1000
WITH Numbers AS
(
    -- Anchor: Start with 1
    SELECT 1 AS n
    UNION ALL
    -- Recursive: Increment until we reach 1000
    SELECT n + 1 FROM Numbers WHERE n < 1000
),
-- Convert numbers to dates for the entire year
DateRange AS
(
    SELECT DATEADD(DAY, n - 1, '2024-01-01') AS OrderDate
    FROM Numbers
    WHERE DATEADD(DAY, n - 1, '2024-01-01') <= '2024-12-31'
)
-- Count orders for each date, showing 0 for dates with no orders
SELECT 
    dr.OrderDate,
    COALESCE(COUNT(soh.SalesOrderID), 0) AS OrderCount
FROM DateRange AS dr
LEFT JOIN SalesLT.SalesOrderHeader AS soh
    ON CAST(soh.OrderDate AS DATE) = dr.OrderDate
GROUP BY dr.OrderDate
ORDER BY dr.OrderDate
OPTION (MAXRECURSION 366);  -- Allow up to 366 iterations (leap year)

استخدم CTEs مع بيانات تعديل البيانات

يمكن استخدام CTEs مع INSERT، ، و DELETE العبارات، وليس فقط SELECTUPDATE. هذه القدرة مفيدة عندما يتطلب منطق التعديل تصفية معقدة أو حسابات:

-- Update using a CTE to identify target rows
WITH DiscontinuedProducts AS
(
    SELECT ProductID
    FROM SalesLT.Product
    WHERE SellEndDate < DATEADD(YEAR, -2, GETDATE())
        AND ProductID NOT IN (
            SELECT DISTINCT ProductID 
            FROM SalesLT.SalesOrderDetail
            WHERE ModifiedDate > DATEADD(YEAR, -1, GETDATE())
        )
)
UPDATE SalesLT.Product
SET DiscontinuedDate = GETDATE()
WHERE ProductID IN (SELECT ProductID FROM DiscontinuedProducts);

يستخدم هذا الاستعلام CTE لتحديد المنتجات التي يجب وسمها كمنتجة متوقفة. يحدد اختبار CTE المنتجات التي كان تاريخ انتهاء بيعها قبل أكثر من عامين ولم تظهر في أي تفاصيل طلبية تم تعديلها خلال العام الماضي. UPDATE ثم يحدد البيان ل DiscontinuedDate ل لتلك الضربات. بفصل منطق الاختيار إلى CTE، يصبح الاستعلام أسهل في القراءة والاختبار بشكل مستقل.

لمزيد من المعلومات حول التعبيرات الشائعة للجداول، راجع WITH common_table_expression (Transact-SQL)والاستعلامات العودية باستخدام تعبيرات الجداول الشائعة.