RyanNerd
라덕'Story
RyanNerd
  • 분류 전체보기 (117)
    • Study Note (80)
      • Python (3)
      • R (1)
      • Airflow (7)
      • 통계 (26)
      • Machine Learning (16)
      • Streamlit (9)
      • Django (1)
      • Deep Learning (12)
      • dbt (2)
      • SQL (3)
    • 빅데이터분석기사 (1)
      • 필기 (1)
    • Programmers (28)
      • Python (13)
      • SQL (15)
    • Project (3)
      • Django (3)
    • Mac (2)

블로그 메뉴

  • NaverBlog
  • 홈

최근 글

전체 방문자
오늘
어제
hELLO · Designed By 정상우.
RyanNerd

라덕'Story

Study Note/SQL

APPLY 연산자란?

2026. 4. 15. 16:42

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를 써야 할까? (핵심 요약)

  1. 동적 서브쿼리 참조: JOIN은 서브쿼리 내에서 외부 테이블의 컬럼을 직접 참조할 수 없지만(Standard SQL 기준), APPLY는 왼쪽 행의 데이터를 서브쿼리 내부 WHERE 절에 파라미터처럼 넣을 수 있습니다.
  2. Top-N 분석의 최강자: "그룹별로 가장 최근 데이터 1개, 혹은 상위 3개"를 가져올 때 가장 직관적이고 효율적입니다.
  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) 보여주는 작업이 매우 고통스러워집니다.

요약: 언제 무엇을 쓸까?

  1. 단순히 두 테이블을 키(Key) 값으로 연결할 때 -> JOIN
  2. 왼쪽 테이블의 값을 이용해 오른쪽에서 '계산'이나 '필터링'을 행별로 수행해야 할 때  ->   APPLY
  3. 그룹 내 상위 N개를 뽑아야 할 때  ->   APPLY
  4. 함수의 결과나 JSON 데이터를 옆에 붙여야 할 때  ->   APPLY

 

'Study Note > SQL' 카테고리의 다른 글

SQL Window Function: Window Frame의 이해  (0) 2026.04.16
SQL 인덱스 가이드: "내 쿼리는 왜 느릴까?"  (1) 2026.02.24
    'Study Note/SQL' 카테고리의 다른 글
    • SQL Window Function: Window Frame의 이해
    • SQL 인덱스 가이드: "내 쿼리는 왜 느릴까?"
    RyanNerd
    RyanNerd
    라이언 덕후의 일상 스토리~

    티스토리툴바