APPLY는 왼쪽 테이블(Outer Table)의 각 행에 대해 오른쪽의 테이블 반환 함수(Table-Valued Function)나 서브쿼리를 실행하여 그 결과를 결합합니다.
- 쉽게 생각하기: JOIN이 두 테이블을 수평으로 이어 붙이는 느낌이라면, APPLY는 왼쪽 테이블의 데이터를 하나씩 꺼내서 오른쪽 로직에 '입력값'으로 전달하는 느낌입니다.
CROSS APPLY vs OUTER APPLY (JOIN과 비교)
JOIN과의 매칭 및 데이터가 없을 때의 동작 방식
| 구분 | 매칭되는 JOIN | 매칭되는 데이터가 없을 떄 (NULL) | 특징 |
| CROSS APPLY | INNER JOIN | 행을 제외함 | 결과값이 있는 행만 출력 |
| OUTER APPLY | LEFT OUTER JOIN | NULL로 표시하여 유지 | 결과가 없어도 왼쪽행은 보존 |
실습 예시: 환자 진료 내역 조회
'환자별 최근 진료 기록 1건'을 가져오는 시나리오로 예시 코드를 작성해 보겠습니다.
1. 데이터 세팅 (Schema)
-- 환자 테이블
CREATE TABLE Patients (
PatientID INT,
PatientName NVARCHAR(50)
);
-- 진료 기록 테이블
CREATE TABLE Visits (
VisitID INT,
PatientID INT,
VisitDate DATE,
Diagnosis NVARCHAR(100)
);
INSERT INTO Patients VALUES (1, '김철수'), (2, '이영희'), (3, '박민수');
INSERT INTO Visits VALUES (101, 1, '2024-01-01', '감기'), (102, 1, '2024-02-15', '비염'), (103, 2, '2024-03-01', '복통');
-- 박민수(3번) 환자는 진료 기록이 없습니다.
2. CROSS APPLY 사용 (매칭되는 데이터만!)
환자별로 최근 진료 기록 1건을 가져옵니다. 진료 기록이 없는 '박민수' 환자는 결과에서 제외됩니다.
SELECT P.PatientName, V.VisitDate, V.Diagnosis
FROM Patients P
CROSS APPLY (
-- 각 환자(P.PatientID)를 입력받아 상위 1건만 반환
SELECT TOP 1 VisitDate, Diagnosis
FROM Visits
WHERE PatientID = P.PatientID
ORDER BY VisitDate DESC
) V;

- 왜 사용했나요?: INNER JOIN으로 최근 1건을 뽑으려면 서브쿼리나 ROW_NUMBER()를 써서 복잡해지지만, CROSS APPLY는 행별로 직접 상위 1건을 찍어낼 수 있어 가독성이 좋습니다.
3. OUTER APPLY 사용 (기록 없어도 출력!)
진료 기록이 없는 환자까지 포함하여 전체 환자 목록을 보고 싶을 때 사용합니다.
SELECT P.PatientName, V.VisitDate, V.Diagnosis
FROM Patients P
OUTER APPLY (
SELECT TOP 1 VisitDate, Diagnosis
FROM Visits
WHERE PatientID = P.PatientID
ORDER BY VisitDate DESC
) V;

- 결과: '박민수' 환자의 진료일과 진단명은 NULL로 표시되며 행은 유지됩니다.
왜 APPLY를 써야 할까? (핵심 요약)
- 동적 서브쿼리 참조: JOIN은 서브쿼리 내에서 외부 테이블의 컬럼을 직접 참조할 수 없지만(Standard SQL 기준), APPLY는 왼쪽 행의 데이터를 서브쿼리 내부 WHERE 절에 파라미터처럼 넣을 수 있습니다.
- Top-N 분석의 최강자: "그룹별로 가장 최근 데이터 1개, 혹은 상위 3개"를 가져올 때 가장 직관적이고 효율적입니다.
- JSON/XML 파싱: 의료 데이터에서 종종 발생하는 XML이나 JSON 형태의 컬럼을 행으로 풀어서 조회할 때 필수적으로 사용됩니다.
4. 서브쿼리에 '매개변수'를 전달할 수 있는 유일한 방법
JOIN은 두 집합을 준비해두고 나중에 붙이는 방식인 반면, APPLY는 왼쪽 행의 값을 오른쪽 서브쿼리의 변수처럼 쓸 수 있게 해줍니다.
JOIN의 한계 (Standard JOIN)
일반적인 JOIN의 서브쿼리는 외부 테이블의 컬럼을 마음대로 가져다 쓸 수 없습니다.
-- 오류가 발생할 수 있는 구조 (Standard SQL 기준)
SELECT P.PatientName, V.VisitDate
FROM Patients P
INNER JOIN (
SELECT TOP 1 VisitDate
FROM Visits
WHERE PatientID = P.PatientID -- 여기서 P.PatientID를 인식하지 못함
ORDER BY VisitDate DESC
) V ON 1=1;
APPLY의 강점
APPLY는 왼쪽 테이블(Patients)의 각 행을 하나씩 읽으면서, 그 행의 PatientID를 오른쪽 서브쿼리에 파라미터로 던져줍니다.
SELECT P.PatientName, V.VisitDate
FROM Patients P
CROSS APPLY (
SELECT TOP 1 VisitDate
FROM Visits
WHERE PatientID = P.PatientID -- 왼쪽 행의 값을 변수처럼 사용 가능!
ORDER BY VisitDate DESC
) V;
5. '그룹별 상위 N개' 추출의 효율성
의료 분석에서는 "모든 환자의 전체 기록"이 아니라 "각 환자별 가장 최근 검사 결과 3개씩" 같은 조건이 자주 붙습니다.
- JOIN으로 할 경우: 모든 데이터를 다 가져온 뒤 ROW_NUMBER()로 순번을 매기고 다시 WHERE 절로 걸러내야 하므로 데이터가 많을수록 연산 낭비가 심해집니다.
- APPLY로 할 경우: 인덱스만 잘 설계되어 있다면, 각 환자별로 딱 3건씩만 찾아오기 때문에 성능상 훨씬 이득을 볼 수 있습니다.
6. 테이블 반환 함수(TVF)와의 결합
이것이 APPLY를 써야 하는 가장 결정적인 이유 중 하나입니다. SQL Server나 다른 DB에서 특정 값을 넣으면 테이블을 뱉어내는 함수를 미리 만들어 두었다면, JOIN으로는 이 함수를 호출할 수 없습니다.
-- 예: 환자의 생년월일을 넣으면 연령대별 건강검진 권고항목 리스트를 반환하는 함수
-- SELECT * FROM dbo.fn_GetCheckupList('1980-01-01')
SELECT P.PatientName, C.CheckupItem
FROM Patients P
CROSS APPLY dbo.fn_GetCheckupList(P.BirthDate) C; -- 함수에 컬럼값을 직접 전달
7. 비정형 데이터(JSON, XML) 파싱
최근 의료 기기나 EMR 데이터는 JSON 형태로 저장되는 경우가 많습니다. 한 행안에 여러 개의 데이터가 뭉쳐 있는 JSON 컬럼을 행으로 풀어서(Unnesting) 보고 싶을 때 APPLY가 쓰입니다.
-- 진료 데이터(Visits) 안에 '처방약' 정보가 JSON 배열로 들어가 있는 경우
SELECT V.VisitID, J.DrugName
FROM Visits V
CROSS APPLY OPENJSON(V.PrescriptionJson) -- JSON 배열을 여러 행으로 쪼개줌
WITH (
DrugName NVARCHAR(50) '$.name'
) J;
만약 APPLY가 없다면 한 환자의 진료 기록 옆에 여러 개의 처방약 리스트를 펼쳐서(Flat) 보여주는 작업이 매우 고통스러워집니다.
요약: 언제 무엇을 쓸까?
- 단순히 두 테이블을 키(Key) 값으로 연결할 때 -> JOIN
- 왼쪽 테이블의 값을 이용해 오른쪽에서 '계산'이나 '필터링'을 행별로 수행해야 할 때 -> APPLY
- 그룹 내 상위 N개를 뽑아야 할 때 -> APPLY
- 함수의 결과나 JSON 데이터를 옆에 붙여야 할 때 -> APPLY
'Study Note > SQL' 카테고리의 다른 글
| SQL Window Function: Window Frame의 이해 (0) | 2026.04.16 |
|---|---|
| SQL 인덱스 가이드: "내 쿼리는 왜 느릴까?" (1) | 2026.02.24 |