Перед тем, как рассмотрим данные варианты соединений, создадим тестовые таблицы в MS SQL Server и заполним их тестовыми данными:
CREATE TABLE Customers (
CustomerId INT PRIMARY KEY,
Name 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'Вера');
INSERT INTO Orders VALUES
(101, 1, '2026-01-10', 500),
(102, 1, '2026-02-15', 700),
(103, 1, '2026-03-20', 300),
(104, 2, '2026-02-01', 1000);
-- у Веры заказов нетMS SQL Server развернут в docker-контейнере на локальном ПК при помощи команды:
docker run -d -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=<your-password>" --name my-mssql-server -p 1433:1433 rapidfort/microsoft-sql-server-2019-ib:latest
OUTER APPLY
OUTER APPLY — это специальный оператор в SQL Server, который выполняет табличную функцию или коррелированный подзапрос для каждой строки основной (левой) таблицы. Поведение идентично LEFT JOIN: если правая часть запроса(подзапрос/функция) не возвращает строк для текущей строки слева, OUTER APPLY все равно сохраняет строку из левой таблицы, заполняя недостающие поля значениями NULL.
Главное отличие OUTER APPLY от обычного LEFT JOIN заключается в том, что правая часть может принимать параметры (столбцы) из левой таблицы.
Для примера выполним запрос:
SELECT c.Name, o.OrderId, o.OrderDate, o.Amount
FROM Customers c
OUTER APPLY (
SELECT TOP (2) OrderId, OrderDate, Amount
FROM Orders
WHERE CustomerId = c.CustomerId -- ссылка на внешнюю таблицу
ORDER BY OrderDate DESC
) o;
И посмотрим, что в результате получилось:

Согласно тестовым данным, которые мы внесли в БД ранее, получили результат, эквивалентный LEFT JOIN: подзапрос выполнился для каждого клиента, а для Веры вернул пустой набор.
CROSS APPLY
CROSS APPLY — не нуждается в детальном описании после того, как мы изучили OUTER APPLY, так как работает почти таким же образом, кроме того, что результат будет схож с INNER JOIN. То есть, если в правой части не найдется пересечений с левой по заданному параметру, то строка из левой таблицы не выведется, в отличие от OUTER APPLY, где поля в правой части заполнились бы значениями NULL.
Для визуализации выполним следующий запрос:
SELECT c.Name, o.OrderId, o.OrderDate, o.Amount
FROM Customers c
CROSS APPLY (
SELECT TOP (2) OrderId, OrderDate, Amount
FROM Orders
WHERE CustomerId = c.CustomerId -- ссылка на внешнюю таблицу
ORDER BY OrderDate DESC
) o;
Как и описано в определении CROSS APPLY, Вера не попала в выборку, так как несмотря на наличие ее в левой части, подзапрос вернул 0 строк и здесь видно, что CROSS APPLY отработал эквивалентно INNER JOIN:

В случае с PostgreSQL это реализовано немного иным образом, который рассмотрим ниже.
OUTER APPLY
-- аналог OUTER APPLY
SELECT c.Name, o.OrderId, o.OrderDate, o.Amount
FROM Customers c
LEFT JOIN LATERAL (
SELECT OrderId, OrderDate, Amount
FROM Orders
WHERE CustomerId = c.CustomerId
ORDER BY OrderDate DESC
LIMIT 2
) o ON TRUE;
CROSS APPLY
-- аналог CROSS APPLY
SELECT c.Name, o.OrderId, o.OrderDate, o.Amount
FROM Customers c
CROSS JOIN LATERAL (
SELECT OrderId, OrderDate, Amount
FROM Orders
WHERE CustomerId = c.CustomerId
ORDER BY OrderDate DESC
LIMIT 2
) o;
Отличия синтаксиса: вместо TOP (2) используется LIMIT 2, а у LEFT JOIN LATERAL обязателен ON TRUE (условие уже внутри подзапроса).