본문 바로가기

소마/강의 기록

[소마 강의 회고] DB JOIN에 대해서 알아보기

강의 개요

멘토 : 강성욱 멘토님
수강일자 : 2026.04.16  19:00시 ~ 22:00시
온/오프라인 : 오프라인

강의 수강 이유

1) DB JOIN 원리 이해를 통한 최적화 방안 알아보기

사실 조인의 종류에 대해서는 잘알고 있지만 조인 자체를 더 잘쓰기 위해서는 어떻게 해야할까? 를 고민해본 적은 크게 없다. 그렇기에 조인을 사용할 때의 주의할 점에 대해 알아보고자 했다. (최적화 방안 등등..)

 

2) DB에 대한 애정

RealMySQL을 읽으면서 DB를 좋아하게 되었다. 특히 인덱스를 딥하게 팠다. 특강에는 DB를 주제로 한 특강이 많지 않아 아쉬웠는데 마침 DBOps 이신 멘토님의 강의가 올라와 흥미가 갔다.

 


강의 핵심 요약

JOIN의 구현 방식 3가지

Nested Loop Join : 이중 반복문을 통한 조인, 드라이빙 테이블은 작게, 드리븐 테이블 조인키에는 인덱스 설정

- Sort Merge Join : 동시에 테이블 결과값 생성후 임시 공간에 정렬 > 스캔으로 결과 생성

- Hash Join : 빌드 테이블 대상 해쉬 테이블 생성 > 프로브 테이블 접근해 결과 생성, 메모리 모자라면 파티션 후 파티션 페어별 머지 수행


강의 내용 상세 요약

 

JOIN의 구현 방식 3가지

Nested Loop Join : 이중 반복문

- Sort Merge Join : 각자 정렬 후 머지

- Hash Join : 해쉬 테이블 생성 후 조인키 매칭을 통해 머지

 


Nested Loop Join(a.k.a NL Join)

- 작은 데이터에 적합

- 복수 테이블에서 하나의 테이블을 기준으로 순차적으로 상대방 Row를 결합하여 조합하는 방식

- 드라이빙 테이블 선정 > Where 절을 통해 조건 필터링 > 드리븐 테이블을 반복적으로 검색하며 최종값 반환

- 드리븐 테이블의 join 컬럼에 인덱스가 있을 때 -> 인덱스를 통해 탐색

- 큰 단위들을 기준으로 한 Nested Loop Join도 존재함

    - Page Nested Loop Join (Page 단위로 Join 생성됨)

    - Block Nested Loop Join(Buffer에 찰 정도의 Page 청크)

for(i=0; i< 드라이빙 테이블 rows; i++) {
    for(j=0; j<드리븐 테이블 rows; j++) {
       // Search
    } 
}

 

유의점

- 반복 CPU 연산이 많으므로 CPU 코어가 부담 > 메모리 부담

- 조인키를 기준으로 드리븐 테이블에 인덱스를 활용한 랜덤 엑세스 기반이므로 대량데이터 처리에 적합하지 않음

- 드라이빙 테이블 선정 주의 : 드라이빙 테이블 행 수만큼 접근이 이루어지므로 최대한 where 절을 통해 줄일 수 있는 테이블일 수록 좋음

- 드리븐 테이블 주의 : 조인키 기반으로 테이블 데이터가 탐색되므로 적절한 인덱스가 설정되어 있어야 함.

 

단점

- 반복문을 돌리므로 CPU 연산이 많음

 

성능 개선 포인트

- 적절한 드라이빙 테이블 선정 : where 절로 최대한 데이터를 거를 수 있는 테이블, 작은 테이블

- 드리븐 테이블 조인 컬럼에 인덱스 설정

 


 

Sort Merge Join(정렬 후 병합 조인)

- Nested loop Join의 랜덤 엑세스를 통한 테이블 반복 접근의 성능 문제를 개선

- 양쪽 테이블을 한번에 접근하여 where 절로 조건 필터링 > 각각의 결과값 정렬 > 정렬 결과를 차례로 스캔

- 양쪽 테이블 결과값을 임시 테이블에서 정렬 > join을 해야하므로 메모리 필요

 

사용 상황

- 대용량 자료 조회

- 조인 조건으로 범위 조건(>, <, >= 등)이 사용되었을 때

- 드리븐 테이블 조인 컬럼에 인덱스가 없거나 인덱스 접근이 효율적이지 못할 때

select /**USER_MERGE(A B) */ A.Color, B.SIZE,...
from TABLE_A A,TABLE_B B
where a.joinkey_a = b.joinkey_b -- join key에 대한 인덱스가 테이블 둘 모두 다 없음
and a.color = 'RED' --인덱스 있음
and b.size = 'MED'; --인덱스 없음

출처 : https://coding-factory.tistory.com/757

 

1) 각 테이블에 접근하여 조건에 맞는 결과값 필터링

    - TABLE_A에 접근하여 인덱스를 활용한 조건 필터링

    - TABLE_B에 접근하여 where 절 조건 필터링

 

2) 각 결과값을 별도의 공간에서 정렬

3) 정렬을 먼저 진행한 테이블부터 차례로 SCAN하며 조인키 기반으로 merge

 

성능 개선 포인트

-SORT_AREA_SIZE 최적화 : 정렬공간을 SORT_AREA_SIZE 설정값만큼 메모리에서 할당받아 사용

- 메모리가 부족하다면 Temporary Table Space를 이용하여 정렬을 수행


 

해쉬 조인

- 한 테이블을 해쉬 테이블로 빌드하고, 다른 테이블 레코드에서 해쉬 값 기반으로 조인 결과 추출

 

MySQL에서 해쉬 조인 지원이 느려진 이유(MySQL 8.0부터 지원)

- 항상 Nested Loop Join보다 빠른 건 아님

- Nested Loop는 첫번째 레코드를 찾는데 시간이 빠르지만 처리시간이 김 -> Response Time이 좋음

- 해쉬 조인은 첫번째 레코드를 찾는데 시간이 많이 걸리지만 처리 시간은 오래 걸리지 않음 -> ThroughPut이 좋음

- MySQL은 주로 범용 DBMS -> Throughput보다 응답시간이 중요하므로 해쉬 조인 철학과 일치되지 않음

=> 따라서 조인 컬럼에 인덱스가 없다거나 조인 대상 테이블 중 일부의 레코드가 매우 적은 경우만 사용

 

실행계획 확인법 : Extra 컬럼에 hash join 태그 확인

 

빌드 단계 - 프로브 단계

- 빌드 단계 : 레코드 건수가 적은 테이블을 해쉬 테이블로 빌드

- 프로브 단계 : 나머지 테이블의 레코드를 읽어 해쉬 테이블의 일치 레코드를 찾음

 

해쉬 테이블을 메모리 안에서 충분히 가능한 경우

- 메모리 내에 빌드 테이블의 해쉬 테이블 생성(해시 키와 결과에 출력될 컬럼들 저장)

- 이후 프로브 테이블을 스캔하여 해쉬 값으로 버킷을 탐색하며 해쉬 체인을 스캔하며 데이터 탐색

 

해쉬 테이블을 생성하기에 메모리가 부족하다면? > 재귀적 청크 후 매핑 (Grace Hash Join)

- 해쉬 테이블은 join_buffer_size 만큼의 버퍼 메모리를 할당받아 사용됨 > 레코드 건수가 많으면 조인 버퍼 공간이 부족

- 이 경우, 다음 2단게를 거침

 

step1) 파티션 단계

- where 절로 양쪽 테이블 필터링

- 해쉬 함수를 적용해 빌드 테이블과 프로블 테이블을 해쉬 값에 따라 파티셔닝 하고 파티션 별로 디스크에 임시 파일로 저장

- 각 테이블에는 동일 해쉬가 적용되며 같은 해쉬 키를 지니게 되며 이렇게 생성된 빌드-프로브 파티션을 Partition Pair라고 함

 

step2) 조인 단계

- Partition Pair에서 한쪽 파티션 파일에 대해 해쉬 함수를 적용해 해쉬 테이블을 생성(옵티마이저가 빌드 테이블 선정)

- Partition Pair에서 반대쪽 파티션 파일의 row를 하나씩 읽으며 해쉬 테이블을 탐색함

- 만약 더 작은 파티션을 메모리에 올리는 과정에서 또 메모리를 초과하는 경우 > 해당 파티션을 대상으로 다시 step1으로 돌아가 추가적인 파티션 진행

 

 

MySQL의 조인 구현 방식은? : Hybrid Hash Join

- 메모리 빌드 > 초과 시 grace hash join을 혼용하는 방식

- 일단 무작정 빌드 테이블 대상으로 해쉬 테이블을 만들어본다.

- 메모리가 초과되면 지금까지 만든 해쉬 테이블은 유지하고, 나머지 데이터를 대상으로 파티션 청킹을 수행

- join을 할때에도 일단 메모리 해쉬 테이블을 기반으로 조인 결과를 생성하고 이후에 파티션 페어별로 해쉬 조인 결과를 머지하여 가져온다.

 


나의 생각

1) Nested Loop Join > Sort Merge Join > Hash Join 순으로 발전해온 조인 방법

처음에는 반복문으로 되었다. 그런데 대량 데이터가 많아지자 조인 쿼리가 느려졌을 것이고, 페이지 단위, 블록 단위로 최대한 버퍼를 잘 활용할 수 있는 방식으로 반복 루프 조인을 개선했다. 그러다 Sort Merge Join이 나와 대량 데이터 분석에 더 짧은 응답 속도를 보장했다. 두 테이블을 한번에 접근해 개선할 수 있다니!

 

그러나 이것으로 충분치 않았다. N-M 관계이거나 정렬 자체에 굉장히 많은 리소스가 드는 경우에는 Sort Merge Join도 부담이 된다. 그래서 Hash Join을 통해 대용량 데이터 쿼리나 fallback을 대체하기 시작했다. 실제로 MySQL 같은 경우에도 조인 키에 인덱스가 걸려있지 않으면 블록 네스티들 루프 조인을 썻다가 MySQL 8.0.17부터 해쉬 조인을 fallback join 방법으로 쓰기 시작했다. 기술의 기술에는 각자 나름의 이유가 있다.

 

2) DB JOIN의 방법에 대해 알아보는 재미

멘토님이 질문을 많이 하면서 강의를 진행하셔서 긴장한 채로 강의를 듣게 되는 게 좋았다. 처음 아이스브레이킹이나 JOIN의 방법까지 설명할때는 대부분 아는내용이라 조금 아쉬웠지만 JOIN 방법론 3가지 설명하는 부분에서 RealMySQL에서 이해되지 않았던 부분들이 확 이해되어 좋았다. 추후 DB Access 강의도 개설하신다고 하셨는데 관심이 생겼다.

 

3) JOIN 컬럼의 인덱스는 필수인 이유

조인 컬럼에 인덱스를 걸면 좋다고 듣긴 했는데 이유를 제대로 이야기하지는 못했다. 다만 Nested Loop Join을 이해하며 왜 join 과정에서 인덱스 설계가 중요한지 인지하게 되었다. DB는 참 알수록 재미있다.

 

 

 

Ref)

https://coding-factory.tistory.com/756

https://coding-factory.tistory.com/757

https://cs186berkeley.net/notes/note9/

https://ryean.tistory.com/73#google_vignette