یادداشت های یک برنامه نویس
دوستدار فرهنگ و هنر کهن ایرانی

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: مثل بالا، ولی ردیف‌های بدون نتیجه هم باید بمانند.

دیدگاه ها: ارسال دیدگاه