مقدمه
تکنولوژی CTE از نسخه SQL Server 2005 رسمیت یافته است و شامل یک result set موقتی[1] است که دارای نام مشخص بوده و میتوان از آن در دستورات SELECT, INSERT, UPDATE, DELETEاستفاده کرد. همچنین از CTE میتوان در دستور CREATE VIEW و دستور SELECT مربوط به آن استفاده کرد. در نسخه SQL Server 2008 نیز امکان استفاده از CTE در دستور MERGE فراهم شده است.
در SQL Serverاز دو نوع CTE بازگشتی[2] و غیر بازگشتی[3] پشتیبانی میشود. در این مقاله سعی شده است نحوه تعریف و استفاده از هر دو نوع آن آموزش داده شود.
انواع روشهای ایجاد جداول موقت
برای استفاده از جداول موقتی در سرور اسکیوال، سه راه زیر وجود دارد.
روش اول: استفاده از دستوری مانند زیر است که سبب ایجاد جدول موقتی در بانک سیستمی tempdb میشود. زمانیکه شما ارتباط خود را با سرور SQL قطع میکنید به صورت اتوماتیک جداول موقت شما از بانک tempdb حذف میشوند. این روش در برنامه نویسی پیشنهاد نمیشود و فقط در کارهای موقتی و آزمایشی مناسب است.
SELECT * INTO #temptable FROM [Northwind].[dbo].[Products]
UPDATE #temptable SET [UnitPrice] = [UnitPrice] + 10
روش دوم: استفاده از متغیر نوع Table است، که نمونه آن در مثال زیر دیده میشود. زمانیکه از محدوده[4] جاری کد[5] خودتان خارج شوید آن متغیر نیز از حافظه پاک میشود. از این روش، عموما در کدهای Stored Procedureها و UserDefined Functionها استفاده میشود.
DECLARE @tempTable TABLE
(
[ProductID] [int] NOT NULL,
[ProductName] [nvarchar](40) NOT NULL,
[UnitPrice] [money] NULL
)
INSERT INTO @tempTable
SELECT
[ProductID],
[ProductName],
[UnitPrice]
FROM [Northwind].[dbo].[Products]
UPDATE @temptable SET [UnitPrice] = [UnitPrice] + 10
روش سوم: استفاده از CTE است که مزیتهایی نسبت به دو روش قبلی دارد و در بخش بعدی به نحوه تعریف و استفاده از آن خواهیم پرداخت.
کار با CTE
ساده ترین شکل تعریف یک CTE به صورت زیر است:
WITH yourName [(Column1, Column2, ...)]
AS
(
your query
)
با کلمه WITH شروع شده و یک نام اختیاری به آن داده میشود. سپس فهرست فیلدهای جدول موقت را درون زوج پرانتز، مشخص میکنید. تعریف این فیلدها اختیاری است و اگر حذف شود، فیلدهای جدول موقت، مانند فیلدهای کوئری مربوطه خواهد بود.
your query شامل دستوری است که سبب تولید یک result set میشود. قواعد تعریف این کوئری مشابه قواعد تعریف کوئری است که در دستور CREATE VIEW کاربرد دارد.
شکل کلی دستور
همانطور که از این تصویر مشخص است میتوان چندین بلوک از این ساختار را به دنبال هم تعریف نمود که با کاما از هم جدا میشوند. در واقع یکی از کاربردهای CTE ایجاد قطعات کوچکی است که امکان استفاده مجدد را به شما داده و میتواند سبب خواناتر شدن کدهای پیچیده شود.
یکی دیگر از کاربردهای CTE آنجایی است که شما نمیخواهید یک شی Viewی عمومی تعریف کنید و در عین حال میخواهید از مزایای Viewها بهرمند شوید.
و همچنین از کاربردهای دیگر CTE تعریف جدول موقت و استفاده از آن جدول به صورت همزمان در یک دستور است.
بعد از آنکه CTE یا CTEهای خودتان را تعریف کردید آنگاه میتوانید مانند جداول معمولی از آنها استفاده کنید. استفاده از این جداول توسط دستوری خواهد بود که دقیقا بعد از تعریف CTE نوشته میشود.
ایجاد یک CTE غیر بازگشتی[6]
مثال اول، یک CTE غیر بازگشتی ساده را نشان میدهد.
WITH temp
AS
(
SELECT
[ProductName],
[UnitPrice]
FROM [Northwind].[dbo].[Products]
)
SELECT * FROM temp
ORDER BY [UnitPrice] DESC
مثال دوم نمونهای دیگر از یک CTE غیر بازگشتی است.
WITH orderSales (OrderID, Total)
AS
(
SELECT
[OrderID],
SUM([UnitPrice]*[Quantity]) AS Total
FROM [Northwind].[dbo].[Order Details]
GROUP BY [OrderID]
)
SELECT
O.[ShipCountry],
SUM(OS.[Total]) AS TotalSales
FROM [Northwind].[dbo].[Orders] AS O INNER JOIN [orderSales] AS OS
ON O.[OrderID] = OS.[OrderID]
GROUP BY O.[ShipCountry]
ORDER BY TotalSales DESC
هدف این کوئری، محاسبه کل میزان فروش کالاها، به ازای هر کشور میباشد. ابتدا از جدول Order Details مجموع فروش هر سفارش محاسبه شده و نتیجه آن در یک CTE به نام orderSales قرار میگیرد و از JOIN این جدول موقت با جدول Orders محاسبه نهایی انجام شده و نتیجهای مانند این تصویر حاصل میشود.
نتیجه خروجی
مثال سوم استفاده از دو CTE را به صورت همزمان نشان میدهد:
WITH customerList
AS
(
SELECT
[CustomerID],
[ContactName]
FROM [Northwind].[dbo].[Customers]
WHERE [Country] ='UK'
)
,orderList
AS
(
SELECT
[CustomerID],
[OrderDate]
FROM [Northwind].[dbo].[Orders]
WHERE YEAR([OrderDate])< 2000
)
SELECT
cl.[ContactName],
YEAR(ol.[OrderDate]) AS SalesYear
FROM customerList AS cl JOIN orderList AS ol
ON cl.[CustomerID] = ol.[CustomerID]
مثال چهارم استفاده مجدد از یک CTE را نشان میدهد. فرض کنید جدولی به نام digits داریم که فقط یک فیلد digit دارد و دارای 10 رکورد با مقادیر 0 تا 9 است. مانند تصویر زیر
نتیجه خروجی
حال میخواهیم از طریق CROSS JOIN اعداد 1 تا 100 را با استفاده از مقادیر این جدول تولید کنیم. کد زیر آنرا نشان میدهد:
WITH digitList
AS
(
SELECT [digit] from [digits]
)
SELECT
a.[digit] * 10 + b.[digit] + 1 AS [Digit]
FROM [digitList] AS a CROSSJOIN [digitList] AS b
در این کد یک CTE تعریف شده و دو بار مورد استفاده قرار گرفته است. مثلا اگر بخواهید اعداد 1 تا 1000 را تولید کنید میتوانید سه بار از آن استفاده کنید. حاصل این دستور result setی مانند زیر است.
نتیجه
نتیجه
حتی میتوان از یک CTE در کوئری CTE بعدی مانند کد زیر استفاده کرد.
WITH CTE_1 AS
(
....
),
CTE_2 AS
(
SELECT ... FROM CTE_1 JOIN ...
)
SELECT *
FROM FOO
LEFTJOIN CTE_1
LEFTJOIN CTE_2
ایجاد یک CTE بازگشتی[7]
از CTE بازگشتی برای پیمایش جداولی استفاده میشود که رکوردهای آن دارای رابطه سلسله مراتبی یا درختی است. نمونه این جداول، جدول کارمندان است که مدیر هر کارمند نیز مشخص شده است یا جدولی که ساختار سازمانی را نشان میدهد یا جدولی که موضوعات درختی را در خود ذخیره کرده است. یکی از مزایای استفاده از CTE بازگشتی، سرعت کار آن در مقایسه با روشهای پردازشی دیگر است.
ساختار کلی یک دستور CTE بازگشتی به صورت زیر است.
WITH cteName AS
(
query1
UNION ALL
query2
)
در بدنه CTE حداقل دو عضو[8] (کوئری) وجود دارد که بایستی با یکی از عبارتهای زیر به هم متصل شوند.
UNION
UNION ALL
INTERSECT
EXCEPT
query1 شامل دستوری است که اولین سری از رکوردهای result set نهایی را تولید میکند. اصطلاحا به این کوئری anchor memberمیگویند.
بعد از دستور query1، حتما بایستی از UNION ALL و امثال آنها استفاده شود.
سپس query2 ذکر میشود. اصطلاحا به این کوئری recursive member گفته میشود. این کوئری شامل دستوری است که سطوح بعدی درخت را تولید خواهد کرد. این کوئری دارای شرایط زیر است.
- حتما بایستی به CTE که همان cteName است اشاره کرده و در جایی از آن استفاده شده باشد. به عبارت دیگر از رکوردهای موجود در جدول موقت استفاده کند تا بتواند رکوردهای بعدی را تشخیص دهد.
- حتما بایستی مطمئن شوید که شرایط کافی برای پایان حلقه پیمایش رکوردها را داشته باشد در غیر این صورت سبب تولید حلقه بی پایان[9] خواهد شد.
بدنه CTE میتواند حاوی چندین anchor member و چندین recursive member باشد ولی فقط recursive memberها هستند که به CTE اشاره میکنند.
برای آنکه نکات فوق روشن شود به مثالهای زیر توجه کنید.
فرض کنید جدولی از کارمندان و مدیران آنها داریم که به صورت زیر تعریف و مقداردهی اولیه شده است.
IFOBJECT_ID('Employees','U')ISNOTNULL
DROPTABLE dbo.Employees
GO
CREATETABLE dbo.Employees
(
EmployeeID intNOTNULLPRIMARYKEY,
FirstName varchar(50)NOTNULL,
LastName varchar(50)NOTNULL,
ManagerID intNULL
)
GO
INSERTINTO Employees VALUES (101,'Alireza','Nematollahi',NULL)
INSERTINTO Employees VALUES (102,'Ahmad','Mofarrahzadeh', 101)
INSERTINTO Employees VALUES (103,'Mohammad','BozorgGhommi', 102)
INSERTINTO Employees VALUES (104,'Masoud','Narimani', 103)
INSERTINTO Employees VALUES (105,'Mohsen','Hashemi', 103)
INSERTINTO Employees VALUES (106,'Aref','Partovi', 102)
INSERTINTO Employees VALUES (107,'Hosain','Mahmoudi', 106)
INSERTINTO Employees VALUES (108,'Naser','Pourali', 106)
INSERTINTO Employees VALUES (109,'Reza','Bagheri', 102)
INSERTINTO Employees VALUES (110,'Abbas','Najafian', 102)
مثال اول: میخواهیم فهرست کارمندان را به همراه نام مدیر آنها و شماره سطح درخت نمایش دهیم. کوئری زیر نمونهای از یک کوئری بر اساس CTE بازگشتی میباشد.
WITHcteReports(EmpID, FirstName, LastName, MgrID, EmpLevel)
AS
(
SELECT EmployeeID, FirstName, LastName, ManagerID, 1
FROM Employees
WHERE ManagerID ISNULL
UNIONALL
SELECT e.EmployeeID, e.FirstName, e.LastName, e.ManagerID,r.EmpLevel + 1
FROM Employees e INNERJOINcteReports r
ON e.ManagerID = r.EmpID
)
SELECT
FirstName +' '+ LastName AS FullName,
EmpLevel,
(SELECT FirstName +' '+ LastName FROM Employees
WHERE EmployeeID = cteReports.MgrID)AS Manager
FROMcteReports
ORDERBY EmpLevel, MgrID
کوئری اول در بدنه CTE رکورد مدیری را میدهد که ریشه درخت بوده و بالاسری ندارد و شماره سطح این رکورد را 1 در نظر میگیرد.
کوئری دوم در بدنه CTE از یک JOIN بین Employees و cteReports استفاده کرده و کارمندان زیر دست هر کارمند قبلی (فرزندان) را بدست آورده و مقدار شماره سطح آنرا به صورت Level+1 تنظیم میکند.
در نهایت با استفاده از CTE و یک subquery جهت بدست آوردن نام مدیر هر کارمند، نتیجه نهایی تولید میشود.
مثال دوم: میخواهیم شناسه یک کارمند را بدهیم و نام او و نام مدیران وی را به عنوان جواب در خروجی بگیریم.
WITHcteReports(EmpID, FirstName, LastName, MgrID, EmpLevel)
AS
(
SELECT EmployeeID, FirstName, LastName, ManagerID, 1
FROM Employees
WHERE EmployeeID = 110
UNIONALL
SELECTe.EmployeeID, e.FirstName, e.LastName, e.ManagerID,r.EmpLevel + 1
FROM Employees e INNERJOINcteReports r
ON e.EmployeeID = r.MgrID
)
SELECT
FirstName +' '+ LastName AS FullName,
EmpLevel
FROMcteReports
ORDERBY EmpLevel
اگر دقت کنید اولین تفاوت در خط اول مشاهده میشود. در اینجا مشخص میکند که اولین سری از رکوردها چگونه انتخاب شود. مثلا کارمندی را میخواهیم که شناسه آن 110 باشد.
دومین تفاوت اصلی این کوئری با مثال قبلی، در قسمت دوم دیده میشود. شما میخواهید مدیر (پدر) کارمندی که در آخرین پردازش در جدول موقت قرار گرفته است را استخراج کنید.
[1] a temporary named result set
[2] recursive
[3] nonrecursive
[4] Scope
[5]مثلا محدوده کدهای یک روال یا یک تابع
[6] nonrecursive
[7] recursive
[8] member
[9] Infinite loop