SQL 쿼리를 작성하다 보면 일반적인 JOIN만으로는 해결하기 어려운 복잡한 상황에 맞닥뜨립니다.
예를 들어 사용자별로 최근 3개의 주문 데이터만 가져오고 싶을 때 같은 경우
join으로 하자니 log 테이블에 존재 하는 걸 왼쪽에 두고 전부 set 해서 연결 하고 거기에서 다시 최근 3개를 뽑기가 어렵습니다.
이때 쓰는 것이 바로 APPLY 연산자입니다.
오늘은 APPLY의 개념과 JOIN과의 차이점, 그리고 어떤 상황에서 APPLY를 써야 하는지 알아보자
APPLY 연산자는 왼쪽 테이블의 각 행에 대해 오른쪽의 테이블 반환 함수나 서브쿼리를 적용할 때 사용합니다.
가장 큰 차이는 오른쪽 항이 왼쪽 항의 데이터를 참조 할수 있는지 입니다.
실무에서 가장 많이 쓰이는 패턴입니다.
모든 고객별로 가장 최근에 주문한 2건씩만 보여달라는 요청이 올 때 JOIN은 한계가 있습니다.
JOIN 사용 시: ROW_NUMBER를 쓰고 번호를 부여 한 이후에 바깥에서 한 번 더 감싸야 해서 복잡합니다.
-- JOIN + 윈도우 함수 방식
SELECT CustomerName, OrderID, OrderDate
FROM (
SELECT C.CustomerName, O.OrderID, O.OrderDate,
ROW_NUMBER() OVER(PARTITION BY C.CustomerID ORDER BY O.OrderDate DESC) as rn
FROM Customers C
INNER JOIN Orders O ON C.CustomerID = O.CustomerID
) AS T
WHERE rn <= 2; -- 순위가 2위 이내인 것만 필터링
APPLY 사용 시: 쿼리가 훨씬 직관적입니다.
-- CROSS APPLY 방식
SELECT C.CustomerName, O.OrderID, O.OrderDate
FROM Customers C
CROSS APPLY (
SELECT TOP 2 *
FROM Orders O
WHERE O.CustomerID = C.CustomerID -- 왼쪽 테이블 C의 ID를 직접 참조!
ORDER BY O.OrderDate DESC
) O;
상품 가격에 부가세를 더한 값을 구하고 그 값을 기준으로 또 다른 계산을 해야 할 때 join에서는 다시 계산해야하지만
JOIN 사용시: 중복 계산
일반적인 SELECT문에서는 별칭(Alias)을 바로 다음 컬럼 계산에 쓸 수 없습니다.
그래서 똑같은 계산식을 계속 반복해서 써야 합니다.
SELECT
Price,
Price * 1.1 AS PriceWithTax,
(Price * 1.1) * 0.1 AS DiscountAmount -- PriceWithTax를 못 써서 또 계산함
FROM Products;
APPLY 방식: 변수처럼 활용
SELECT
P.Price,
A.PriceWithTax,
A.PriceWithTax * 0.1 AS DiscountAmount
FROM Products P
CROSS APPLY (
SELECT P.Price * 1.1 AS PriceWithTax -- 여기서 계산하고 위에서 써먹음
) A;
이경에는 간단한거는 사실 그냥 join써도 뭐 괜찮은거같긴한경우인데 복잡해지거 쓰는게 많아질경우 정도에만 사용해도 된다.
사용자 ID를 넣으면 그 사람의 최근 로그 기록을 테이블로 반환하는 함수가 있을 때
SELECT U.UserName, L.*
FROM Users U
CROSS APPLY dbo.fn_GetUserLogs(U.UserID) L; -- 각 유저의 ID를 함수에 전달
이경우에는 join은 바로 연결 못하는경우이다 그냥 apply사용을 해야한다.
JOIN은 두 집합을 옆으로 붙이는 것이고 APPLY는 왼쪽의 각 행을 위해 오른쪽 기능을 매번 실행해 주는 것이다.
| 상황 | 추천 연산자 | 이유 |
|---|---|---|
| 단순히 두 테이블을 합칠 때 | JOIN | 표준 SQL이며 가장 빠르고 기본적임 |
| 그룹별 TOP N을 구할 때 | APPLY | 윈도우 함수보다 직관적이고 관리가 편함 |
| 함수에 파라미터를 넘길 때 | APPLY | JOIN으로는 구현 자체가 불가능함 |
| 계산식이 너무 복잡할 때 | APPLY | 서브쿼리 내에서 계산 후 별칭으로 재사용 가능 |