431. 중첩 루프 조인 (Nested Loop Join)
⚠️ 이 문서는 데이터베이스가 두 개의 테이블을 합칠(JOIN) 때 가장 기본적으로 사용하는 무식하지만 확실한 방법인, **바깥쪽 테이블(Outer)을 한 줄씩 읽으면서 안쪽 테이블(Inner)을 처음부터 끝까지 매번 반복해서 뒤지는 '중첩 루프 조인'**을 다룹니다.
핵심 인사이트 (3줄 요약)
- 본질: 프로그래밍의
for문 안에for문이 있는 것과 완벽히 똑같은 구조다. (이중 루프)- 조건: 안쪽 테이블(Inner Table)의 조인 컬럼에 반드시 인덱스가 있어야만 쓸 만한 속도가 나온다. 인덱스가 없으면 지옥 같은 풀 스캔이 발생한다.
- 특징: 데이터양이 적을 때 가장 빠르며, 첫 번째 결과가 나오는 응답 시간(Initial Response Time)이 0.001초 수준으로 매우 빨라서 웹 서비스(OLTP)에서 가장 사랑받는 조인 방식이다.
Ⅰ. 개요: 100번의 소개팅 (Context & Necessity)
부서 테이블(데이터 10건)과 직원 테이블(데이터 1만 건)을 조인하려고 한다.
중첩 루프 조인(NL Join)은 이렇게 행동한다.
부서테이블에서 1번 부서(영업팀)를 꺼낸다.직원테이블을 뒤져서 1번 부서 사람들을 다 찾아서 합친다.- 다시
부서테이블에서 2번 부서(개발팀)를 꺼낸다. - 다시
직원테이블을 뒤져서 2번 부서 사람들을 다 찾아서 합친다.
이 짓을 부서 개수(10번)만큼 반복한다. 가장 무식해 보이지만, **'인덱스'**라는 무기가 합쳐지면 이야기가 달라진다. 직원 테이블에 인덱스가 있다면 1만 명을 다 뒤지는 게 아니라, 0.01초 만에 1번 부서 사람들을 쏙쏙 뽑아올 수 있기 때문이다.
📢 섹션 요약 비유: NL 조인은 **'우편물 배달'**과 같습니다. 우체부 아저씨(옵티마이저)가 주소 10개가 적힌 수첩(Outer 테이블)을 들고, 1번 주소로 가서 편지를 넣고(Inner 테이블 탐색), 2번 주소로 가서 편지를 넣는 행동을 반복하는 직관적인 방식입니다.
Ⅱ. NL 조인의 핵심 메커니즘 ★
이 조인이 성공하려면 **'누가 드라이빙(Driving)을 할 것인가'**가 모든 것을 결정한다.
1. 선행 테이블 (Driving Table / Outer Table)
- 정의: 이중 루프의 **'바깥쪽'**에 있는 테이블이다. 이 테이블에서 먼저 데이터를 걸러낸다.
- 규칙: 무조건 데이터가 적은 테이블을 선행 테이블로 잡아야 한다. 바깥쪽 루프가 10번 도는 것과 1만 번 도는 것은 천지 차이이기 때문이다.
2. 후행 테이블 (Driven Table / Inner Table)
- 정의: 이중 루프의 **'안쪽'**에 있는 테이블이다. 선행 테이블에서 넘어온 값을 받아 계속 검색 당하는 역할이다.
- 규칙: 무조건 조인 컬럼에 '인덱스'가 있어야 한다. 인덱스가 없으면 선행 테이블이 100건일 때 후행 테이블을 100번 풀 스캔(Full Scan)해야 하므로 서버가 폭발한다.
Ⅲ. 랜덤 I/O의 저주
NL 조인은 치명적인 약점을 가지고 있다. 바로 디스크 바늘이 이리저리 널뛰는 랜덤 I/O가 너무 많이 발생한다는 점이다.
- 선행 테이블에서 조건에 맞는 데이터 10건을 찾았다 치자.
- 후행 테이블의 인덱스를 타고 들어가서 진짜 주소(ROWID)를 얻는다.
- 그 주소를 가지고 디스크로 점프(Key Lookup, 423번 문서)해서 10번을 꺼내온다.
만약 조인 결과가 10만 건이라면? 10만 번의 랜덤 점프가 발생한다. 이 때문에 **대용량 데이터 조인에서는 NL 조인을 절대 쓰면 안 되며, 해시 조인(Hash Join - 433번 문서)이나 소트 머지 조인(Sort Merge Join - 432번 문서)**을 써야 한다.
┌──────────────────────────────────────────────────────────────┐
│ 중첩 루프 조인 (Nested Loop Join) 작동 시각화 │
├──────────────────────────────────────────────────────────────┤
│ │
│ FOR (부서 IN 선행_테이블 WHERE 조건) { ◀── (여기서 10명 찾음) │
│ │
│ FOR (직원 IN 후행_테이블_인덱스 WHERE 부서ID = 부서.ID) { │
│ ◀── (인덱스 덕분에 안쪽 루프는 빛의 속도로 끝남) │
│ 결과_리턴(부서 + 직원); │
│ } │
│ } │
│ │
│ ★ 특징: 가장 먼저 조건에 맞는 1줄을 찾는 속도(Initial Response)는 │
│ 모든 조인 방식 중 가장 빠르다! (페이징 처리에 유리함) │
└──────────────────────────────────────────────────────────────┘
Ⅳ. 결론
"작은 것에서 시작하여, 인덱스로 파고들어라."
관계형 데이터베이스에서 가장 기본이 되는 조인 방식이다. 실시간으로 수십만 명의 접속자가 작은 데이터를 빠르게 치고 빠지는 OLTP(웹/앱 서비스) 환경에서는 NL 조인만큼 훌륭한 알고리즘이 없다. 백엔드 개발자가 조인 쿼리를 짤 때 WHERE 절에 쓴 조건들이 인덱스를 타서 '선행 테이블'의 범위를 확 줄여줄 수 있는지, 그리고 조인하는 '후행 테이블'의 FK 컬럼에 인덱스가 잘 걸려 있는지 확인하는 것만으로도 대부분의 쿼리 튜닝은 끝이 난다.
📌 관련 개념 맵
- 관련 연산: 관계 대수 JOIN ($\bowtie$, 409번 문서)
- 비교 방식: Hash Join (433번), Sort Merge Join (432번)
- 핵심 키워드: Driving Table (선행 테이블), Random I/O
- 튜닝 힌트 (Oracle):
/*+ USE_NL(A B) */
👶 어린이를 위한 3줄 비유 설명
- 중첩 루프 조인은 선생님이 '1반 학생 명부(선행 테이블)'를 들고 도서관에 가는 거예요.
- 1번 학생 이름을 부르고, 도서관 장부(후행 테이블)에서 대출 기록을 쓱 찾아서 적어요. 그다음 2번 학생을 부르고 또 찾고 적어요.
- 이 방식은 반 학생이 10명일 땐 엄청 빠르지만, 학생이 1만 명이면 선생님이 도서관 장부를 1만 번이나 넘겨봐야 해서 쓰러져버린답니다!