APPLY چیست؟
عملگر APPLY به شما اجازه میدهد برای هر ردیف از جدول سمت چپ، یک table-valued function یا subquery (derived table) را اجرا کنید؛ و نکته کلیدی اینکه عبارت سمت راست میتواند به ستونهای ردیف سمت چپ ارجاع بدهد. این کار با JOIN معمولی ممکن نیست.
CROSS APPLY: شبیه INNER JOIN است؛ اگر سمت راست برای یک ردیف هیچ نتیجهای نداشت، آن ردیف از خروجی حذف میشود.
OUTER APPLY: شبیه LEFT JOIN است؛ اگر سمت راست نتیجهای نداشت، ردیف سمت چپ با NULL در ستونهای سمت راست نمایش داده میشود.
ساخت دادههای نمونه:
CREATE TABLE Customers (
CustomerID INT PRIMARY KEY,
CustomerName NVARCHAR(50)
);
CREATE TABLE Orders (
OrderID INT PRIMARY KEY,
CustomerID INT,
OrderDate DATE,
Amount DECIMAL(10,2)
);
INSERT INTO Customers VALUES
(1, N'علی'), (2, N'مهران'), (3, N'رضا'), (4, N'عرفان');
INSERT INTO Orders VALUES
(101, 1, '2026-01-10', 500),
(102, 1, '2026-02-15', 300),
(103, 1, '2026-03-20', 900),
(104, 2, '2026-01-25', 700),
(105, 2, '2026-04-02', 200),
(106, 3, '2026-03-11', 1200);
-- عرفان (۴) هیچ سفارشی ندارد
مثال، آخرین سفارش هر مشتری:
CROSS APPLY
SELECT c.CustomerName, o.OrderID, o.OrderDate, o.Amount
FROM Customers c
CROSS APPLY (
SELECT TOP 1 *
FROM Orders o
WHERE o.CustomerID = c.CustomerID
ORDER BY o.OrderDate DESC
) o;
نتیجه: علی، مهران و رضا هر کدام یک ردیف دارند. عرفان نمایش داده نمیشود چون سفارشی ندارد (رفتار Inner).
OUTER APPLY
SELECT c.CustomerName, o.OrderID, o.OrderDate, o.Amount
FROM Customers c
OUTER APPLY (
SELECT TOP 1 *
FROM Orders o
WHERE o.CustomerID = c.CustomerID
ORDER BY o.OrderDate DESC
) o;
نتیجه: این بار عرفان هم نمایش داده میشود با مقدار NULL برای OrderID و OrderDate و Amount.
چرا با JOIN معمولی نمیشود؟
این کوئری خطا میدهد:
SELECT c.CustomerName, o.*
FROM Customers c
INNER JOIN (
SELECT TOP 1 * FROM Orders
WHERE CustomerID = c.CustomerID -- ❌ خطا: c در دسترس نیست
ORDER BY OrderDate DESC
) o ON 1=1;
چون در JOIN سمت راست نمیتواند به سمت چپ ارجاع بدهد. معادل درست با JOIN نیاز به ROW_NUMBER دارد:
SELECT c.CustomerName, x.OrderID, x.OrderDate, x.Amount
FROM Customers c
LEFT JOIN (
SELECT *, ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate DESC) rn
FROM Orders
) x ON x.CustomerID = c.CustomerID AND x.rn = 1;
استفاده با Table-Valued Function
CREATE FUNCTION dbo.fn_TopOrders (@CustomerID INT, @N INT)
RETURNS TABLE
AS RETURN (
SELECT TOP (@N) OrderID, OrderDate, Amount
FROM Orders
WHERE CustomerID = @CustomerID
ORDER BY Amount DESC
);
GO
-- دو سفارش بزرگ هر مشتری
SELECT c.CustomerName, f.*
FROM Customers c
OUTER APPLY dbo.fn_TopOrders(c.CustomerID, 2) f;
اینجا پارامتر تابع از ستون جدول چپ میآید؛ کاربرد اصلی APPLY.
کاربردهای رایج
الف) Top N در هر گروه
مثال بالا (آخرین یا بزرگترین N رکورد هر مشتری).
ب) محاسبه ستون مشتقشده و استفاده مجدد (Alias reuse)
SELECT o.OrderID, o.Amount, t.Tax, t.Total
FROM Orders o
CROSS APPLY (SELECT o.Amount * 0.09 AS Tax) a
CROSS APPLY (SELECT o.Amount + a.Tax AS Total, a.Tax) t;
پ) باز کردن (Unpivot) ستونها
SELECT p.ProductID, v.Quarter, v.Sales
FROM ProductSales p
CROSS APPLY (VALUES
('Q1', p.Q1), ('Q2', p.Q2), ('Q3', p.Q3), ('Q4', p.Q4)
) v(Quarter, Sales);
ت) کار با STRING_SPLIT و JSON/XML
SELECT t.ID, s.value
FROM Tags t
CROSS APPLY STRING_SPLIT(t.TagList, ',') s;
SELECT j.Id, x.[key], x.value
FROM JsonTable j
CROSS APPLY OPENJSON(j.JsonData) x;
ث) استفاده از DMVها
SELECT qs.execution_count, st.text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st;
نکات عملکردی (Performance)
APPLY اغلب به صورت Nested Loops اجرا میشود؛ برای هر ردیف چپ یک بار سمت راست ارزیابی میشود.
اگر سمت راست ایندکس مناسب داشته باشد (مثلاً روی Orders(CustomerID, OrderDate DESC))، بسیار سریع است.
برای کار روی حجم بالای ردیفها با تجمیع ساده، JOIN + GROUP BY یا ROW_NUMBER گاهی سریعتر است چون میتواند Hash Join یا Merge Join استفاده کند.
optimizer گاهی CROSS APPLY را به JOIN تبدیل میکند؛ همیشه Execution Plan را بررسی کنید.
از Multi-statement TVF پرهیز کنید (کند هستند)؛ Inline TVF را ترجیح دهید.
چه زمانی کدام را انتخاب کنیم؟
JOIN: ارتباط ساده بین دو جدول با شرط برابری روی کلید.
CROSS APPLY: وقتی سمت راست به سمت چپ وابسته است و فقط ردیفهای دارای نتیجه را میخواهید.
OUTER APPLY: مثل بالا، ولی ردیفهای بدون نتیجه هم باید بمانند.