조인 시리즈 첫 번째 주제는 NL 조인이다.
가장 기본적이고, 자주 사용되는 조인 방식이지만,
다른 조인(소트 머지 조인, 해시 조인)에 비해 느리다는 오명으로 사용을 기피(?) 하기도 하는 조인 방식이다
이번 글에선 NL 조인의 AtoZ 까지는 아니지만.. AtoG 정도까지는 다뤄볼 예정이다
느리다는 오명은 왜 생긴거고, 어떻게 사용해야 하는지 알아보자
먼저 NL 조인은 Nested Loop라는 이름에서 유추할 수 있듯이
조인하고자 하는 A, B 테이블을 Loop를 돌며 건건이 찾아 조인하는 방식이다
그러한 특징으로 인해 A,B 테이블의 인덱스 사용이 거의 필수적인 것이 특징이다. (상황에 따라 인덱스를 사용하지 않는 케이스도 있다 - Sorintg 된 테이블을 Full Scan이 더 효율적이라거나..)
기본적인 동작 방식을 A,B 테이블이 서로 NL 조인하는 예시로 알아보자 ( A = Driving , B = Inner, index = Using A_X1, B_X1 )
- A_X1 index Access
- A_X1 index Filter
- A - Table Access (By RowId) & Filter
- 3번의 결과 단건 기준 B_X1 조인
- B_X1 Access
- B_X1 Filter
- B - Table Access (By RowId) & Filter
- 결과 집합 포함
- 2 번 ~ 8번 반복
위 1~9 번 까지의 과정이 NL 조인의 기본적인 동작 메커니즘이고, 놀랍게도 4번을 제외한 나머지 모든 과정이 성능 개선이 가능한 포인트다 (그만큼 성능을 고려할 때 검토해야 할 요소가 많다는 뜻이기도 하다)
또 한가지 확인할 수 있는 가장 큰 특징은 Driving 테이블 건건이, Inner Table 건건이 데이터에 Access 해서 결과 집합에 포함하는 점이다
평범한 개발자라면 이미 건건이라는 단어에서 경기를 일으켰을 수도 있다
하지만 경기를 일으키면 안된다. 나중에 설명하겠지만 최종적으로 우리는 NL 조인을 최대한 효율적으로, 잘 사용할 수 있는가를 고민해야 한다
결과 집합을 건건이 구성하는 것에는 가장 큰 단점이 있다
그것은 바로 대량 데이터 조회 시 성능 문제이다
대량 데이터 조회 시 흔히 문제가 되는 부분은 바로 3번 7번 과정이다.
인덱스에서 얻은 RowId 기준으로 Table을 Access 할 때, 기본적으로 래치 획득 후 SGA의 Buffer를 뒤지지만 Buffer에 없는 데이터에 접근하려면 Table Random Access에 Single Block I/O 가 발생한다
이는 1개 Block에 1000개의 데이터가 들어있다면 A 데이터 10 개를 읽기 위해 10개 Block 에 Random Access를 하게 되고, 이때 불러오는 데이터의 수는 10000개이다
즉 10개 데이터를 불러오기 위해 9990개의 비효율을 감수해야 하는 것이다
하지만 인덱스를 활용해 결과 집합을 즉시 생성하는 NL 조인 특성상 장점도 있다
바로 소량 데이터를 조회하는 경우와, 조회 빈도수가 많은 경우이다
인덱스를 기준으로 데이터를 건건이 읽어 바로 결과집합을 구성하기에 결과 집합 구성 속도가 빠르고,
기본적으로 물리적으로 생성되어 재사용이 가능한 인덱스를 사용하기에 다른 조인 방식에 비해 CPU, 메모리 효율이 높다
추가로 위에서 언급한 건건이 Table Access 의 단점을 보완할 수 있는 기능들 또한 추가되었다
Oracle을 예시로 보면, prefech 와 batch Access 기능을 제공하는데
prefetch는 앞으로 읽을 Block을 미리 Buffer로 불러와 Buffer를 경유해 Table Access로 가는 과정을 최소화하고,
batch Access는 Block I/O 를 Batch로 처리해 Table Access 비용을 줄이는 데 도움을 준다. (물론 Batch I/O로 인한 결과 집합 정렬 미보장 이슈는 의도적으로 핸들링해줘야 한다)
또, Index 구성을 변경하는 것만으로도 쿼리 효율을 극대화할 수 있다는 것도 장점이다 (제대로 사용하지 못하면 단점이다)
예를 들어 3번, 7번에 Table Access가 필요한 컬럼을 Index에 추가한다면 모든 과정을 Index 내에서 해결이 가능해 쿼리 성능이 올라간다
추가로 1,2번의 Index Access, Filter를 효율적으로 만들기 위해 Index 순서를 구성하는 것도 매우 중요하다
제대로 구성되지 않은 Index는 Full Scan 보다 더 극심한 성능 이슈를 발생시킬 수 있다
NL 조인은 Driving 테이블 -> Inner 테이블을 조회하는 특성상,
Driving 테이블에 Filter 된 데이터가 작을수록 그 효율이 극대화된다.
위 예시를 다시 가져와보자면 (5~8번) Inner 테이블을 Filter 해야 하는 데이터가 1000 건씩 고정적으로 있다고 가정하면
(4번) Driving 테이블에 Filter 된 데이터가 10건인 것과, 1000건 인 것의 성능은 숫자 그대로 100배의 차이를 보일 것이다
(10 * 1000 = 총 10000번의 일 량, 1000 * 1000 = 총 1000000번의 일 량)
그래서 다시 말하지만 Driving 테이블의 데이터에 따라 전체 일 량이 결정된다
위에서 언급한 모든 특성을 종합해 보면
NL 조인은 튜너의 손을 많이 타는 조인 방식이고, 그에 따른 성능 차이도 극과 극에 달한다
NL 조인으로 수행한 쿼리가 느리다고 해서, 소트 머지 조인과 해시 조인 도입을 먼저 고려하기보단
실행 계획을 보고 NL 조인을 최대한 튜닝해 본 뒤, 다른 조인 방식을 검토하는 것을 추천한다
(참고로 필자는 NL 조인과 다른 조인 방식의 성능이 동일하거나 NL 조인이 조금 느려도, NL 조인 사용을 추천한다)
'개발 > Database' 카테고리의 다른 글
[Join Series] 3. Hash Join (0) | 2025.04.05 |
---|---|
[Join Series] 2. Sort Merge Join (0) | 2025.03.28 |
[Join Series] 매번 눈에 밟히는 그 조인 시리즈 (0) | 2025.03.18 |
RDB 성능 개선하기 - BETWEEN vs LIKE (0) | 2025.03.08 |
[Spring/Postgres] SKIP Lock 사용하여 동시성 이슈 해결하기 (0) | 2025.03.01 |
댓글