CROSS APPLY vs OUTER APPLY: SQL Server's Secret Weapon
CROSS APPLY vs OUTER APPLY: SQL Server's Secret Weapon for Real-World Queries
Most SQL developers are comfortable with:
JOIN
WHERE
GROUP BY
CTE
But there is another powerful SQL Server feature that is often overlooked:
CROSS APPLY
OUTER APPLY
The easiest way to understand APPLY is not by memorizing its syntax.
Start with a problem.
1. The Problem: Latest Order for Every Customer
Suppose we have:
Customers
----------------
CustomerId
Name
Orders
----------------
OrderId
CustomerId
OrderDate
Amount
We need:
Get every customer with their latest order.
A common solution is a correlated subquery:
SELECT
c.CustomerId,
c.Name,
o.OrderId,
o.OrderDate,
o.Amount
FROM Customers c
JOIN Orders o
ON o.CustomerId = c.CustomerId
WHERE o.OrderDate = (
SELECT MAX(OrderDate)
FROM Orders
WHERE CustomerId = c.CustomerId
);
It works, but the query becomes more complicated as the requirement grows.
For example:
Get the latest order, including its
OrderId,Amount,Status, and other columns.
This is where APPLY becomes interesting.
2. CROSS APPLY
The easiest mental model is:
For each row on the left, execute the right-side query.
SELECT
c.CustomerId,
c.Name,
o.OrderId,
o.OrderDate,
o.Amount
FROM Customers c
CROSS APPLY
(
SELECT TOP 1
OrderId,
OrderDate,
Amount
FROM Orders o
WHERE o.CustomerId = c.CustomerId
ORDER BY OrderDate DESC, OrderId DESC
) o;
Think of it like this:
Customer 1
|
+--> Find latest order
|
+--> Return it
Customer 2
|
+--> Find latest order
|
+--> Return it
Customer 3
|
+--> Find latest order
|
+--> Return it
The right-side query can directly reference the current row from the left side:
WHERE o.CustomerId = c.CustomerId
That correlation is what makes APPLY useful.
3. Why Not Just Use JOIN?
JOIN is excellent when you're matching sets of rows.
But consider this requirement:
Get the 3 latest orders for every customer.
With APPLY:
SELECT
c.CustomerId,
c.Name,
o.OrderId,
o.OrderDate,
o.Amount
FROM Customers c
CROSS APPLY
(
SELECT TOP 3
OrderId,
OrderDate,
Amount
FROM Orders o
WHERE o.CustomerId = c.CustomerId
ORDER BY OrderDate DESC
) o;
The logic is almost exactly how you would describe the requirement:
For each customer
↓
Find TOP 3 orders
↓
Order by latest
↓
Return them
This is one of the most practical uses of CROSS APPLY.
4. CROSS APPLY vs OUTER APPLY
This is the most important difference.
Suppose we have:
Customer 1 → Orders
Customer 2 → Orders
Customer 3 → No Orders
With CROSS APPLY:
SELECT
c.CustomerId,
c.Name,
o.OrderId
FROM Customers c
CROSS APPLY
(
SELECT TOP 1 *
FROM Orders o
WHERE o.CustomerId = c.CustomerId
ORDER BY OrderDate DESC
) o;
Customer 3 disappears because the right-side query returned no rows.
Conceptually:
CROSS APPLY
Customer 1 → Order
Customer 2 → Order
Customer 3 → Not returned
Now change it to:
OUTER APPLY
SELECT
c.CustomerId,
c.Name,
o.OrderId
FROM Customers c
OUTER APPLY
(
SELECT TOP 1 *
FROM Orders o
WHERE o.CustomerId = c.CustomerId
ORDER BY OrderDate DESC
) o;
Now:
OUTER APPLY
Customer 1 → Order
Customer 2 → Order
Customer 3 → NULL
The easiest way to remember:
CROSS APPLY
≈ INNER JOIN behavior
OUTER APPLY
≈ LEFT JOIN behavior
5. A More Practical Example: Top Employees
Suppose we have:
Departments
----------------
DepartmentId
Name
Employees
----------------
EmployeeId
Name
DepartmentId
Salary
Requirement:
Get the top 2 highest-paid employees from every department.
SELECT
d.DepartmentId,
d.Name AS DepartmentName,
e.EmployeeId,
e.Name,
e.Salary
FROM Departments d
CROSS APPLY
(
SELECT TOP 2
EmployeeId,
Name,
Salary
FROM Employees e
WHERE e.DepartmentId = d.DepartmentId
ORDER BY Salary DESC
) e;
Again, the query reads naturally:
For each department
↓
Find TOP 2 employees
↓
Order by salary
↓
Return them
This pattern appears frequently in real applications.
6. APPLY with Table-Valued Functions
APPLY is also useful with table-valued functions.
Suppose we have:
dbo.GetCustomerOrders(CustomerId)
which returns orders for a customer.
We can write:
SELECT
c.CustomerId,
c.Name,
o.OrderId,
o.Amount
FROM Customers c
CROSS APPLY
dbo.GetCustomerOrders(c.CustomerId) o;
The function receives the current customer's ID.
Conceptually:
Customer 1 → GetCustomerOrders(1)
Customer 2 → GetCustomerOrders(2)
Customer 3 → GetCustomerOrders(3)
This is another situation where APPLY provides a natural way to express a correlated operation.
7. The Performance Question
You might be tempted to say:
"
CROSS APPLYis faster thanJOIN."
Don't.
That's not generally true.
APPLY is a query pattern, not a guaranteed performance optimization.
Performance depends on:
Query
↓
Indexes
↓
Data distribution
↓
Execution plan
↓
Actual workload
For example, our latest-order query can benefit from an index such as:
CREATE INDEX IX_Orders_CustomerId_OrderDate
ON Orders(CustomerId, OrderDate DESC)
INCLUDE (Amount);
This can help SQL Server efficiently locate the relevant orders for each customer.
But whether it actually improves performance should be verified using the execution plan and measurements.
8. When Should You Think About APPLY?
When you see requirements like:
For each customer...
find the latest order
For each department...
find the top 3 employees
For each product...
find the latest price
For each account...
find the most recent transaction
stop and consider:
CROSS APPLY
or:
OUTER APPLY
The pattern is especially useful when the right-side query needs to reference the current row from the left side.
9. JOIN vs APPLY
A simple mental model:
JOIN
↓
Match sets of rows
while:
APPLY
↓
For each left-side row,
run a correlated operation
on the right side
And:
CROSS APPLY
↓
No right-side result?
Left row is removed.
OUTER APPLY
↓
No right-side result?
Keep the left row with NULLs.
Final Takeaway
CROSS APPLY and OUTER APPLY are not replacements for JOIN.
They solve a different kind of problem.
When the requirement sounds like:
"For each row on the left, find something from the right."
APPLY should come to mind.
The three patterns worth remembering are:
Latest record per group
↓
CROSS APPLY + TOP 1
Top N per group
↓
CROSS APPLY + TOP N
Keep the parent even when
there is no matching child
↓
OUTER APPLY
And just like any SQL optimization:
Don't assume
APPLYis faster. Check the execution plan, indexes, and actual workload.
The real power of APPLY is that it lets you express row-by-row correlated logic in a clean, set-based SQL query.
Enjoyed this article? Share it with your network!