Block Nested Loop Join에 대해 알아보자.
앞서, Join의 구현 방식 3가지에 대해 알아봤다. NL 조인 방식은 인덱스가 없다면 가장 느린 성능이 나타나는데 이러한 한계를 극복하기 위해 Block Nested Loop Join방식을 활용한다. (Mysql의 경우 8.0.20부터 Block Nested Loop Join 대신 Hash Join을 사용한다고 한다. :출처)
Nested Loop Join은 Drivien Table의 IO 접근을 최소화하는 것이 성능에 중요하다. (이러한 이유로 Driven Table 의 IO 접근에 영향을 주는 Driving Table의 행의 개수를 가능하면 적도록 한다.)
Block Nested Loop Join은 두 테이블을 매번 1:1로 비교하는 것이 아니라 버퍼를 두어 N:1의 비율로 비교하도록 하여 IO를 줄인다.
위 이미지를 살펴보면 N개의 Driving Table의 행들을 모아 버퍼를 만든 후, |S|개의 Driven Table과 비교하여 Join 결과를 획득한다.
이러한 경우, 일반적인 방식에서 소요되는 |R| * |S|
번의 접근 대신 (|R| * |S|)/N + 1
번의 접근이 일어나 더욱 효율적이다. 다만, 버퍼를 많이 유지할 경우 메모리 소모가 크다는 단점이 있다.
실제 파일 입출력을 이용하여 이를 구현해보자. 실습에 사용된 데이터들은 TPC-H 라는 벤치마크를 위한 데이터를 활용하였고, 언어는 C++를 활용하였다.
Driving Table인 Part, Driven Table인 PartSupp가 있고, PARTKEY를 키로 사용한다.
(구현된 모습)
위 이미지가 실제로 구현된 모습이며, 각 객채에 대해 살펴보자.
TableReader
: 테이블의 데이터를 한 줄 씩 읽어와, template class를 활용해 원하는 객체로 반환해주는 객체이다. TableWriter
: 객체를 읽어와 테이블에 저장하는 객체이다.Part
, PartSupp
: 테이블에 저장된 두 데이터 모델(Part, PartSupp)를 구현한 객체이다. 테이블을 읽어 객체로 변환하기 위한 용도로만 활용한다.PartAndPartSupp
: Join 연산의 결과를 저장한다. 내부적으로 Part
, PartSupp
을 유지하고, 이들의 데이터를 테이블의 행 형태로 내보내기 위해 사용한다.BufferManage
은 내부적으로 키:PARTKEY 값:배열 을 유지하는 해시 테이블을 갖고 있는 버퍼 관리 객체이다.
배열을 유지하도록 한 까닭은
TableReader
가 Part.tbl
을 한 줄씩 읽는다.string
)를 Part VO
로 변환한다.BufferManager
가 유지하는 해시 테이블에 Part VO
를 저장한다TableReader
가 PartSupp.tbl
을 한 줄씩 읽는다.읽은 데이터(string)
를 PartSupp VO
로 변환한다.PartSupp VO
를 버퍼에 조회하여 일치하는 Part VO
들의 집합을 찾는다.Part
와 PartSupp VO
를 통해 조인 결과 PartAndPartSupp VO
를 생성한다.TableWriter
에 의해 Output.tbl
에 조인 결과인 PartAndPartSupp VO
를 문자열로 변환하여 작성한다.PartSupp.tbl
를 다 읽을 때 까지 반복한다.Part.tbl
를 다 읽을 때 까지 반복한다.버퍼 크기에 따라 실행시간, 메모리 사용량(버퍼가 꽉찼을 경우의 최대) 를 비교해보자.
버퍼가 작다면
버퍼가 크다면
(결과)
2 | 10 | 1000 | |
---|---|---|---|
Max VM (KB) | 4764 | 4764 | 5160 |
Max RSS (KB) | 1308 | 1324 | 3284 |
Elapsed time (ms) | 1765.09 | 429.3 | 66.925 |
(표로 정리한 모습)
차례대로 가상 메모리 사용량, 프로그램 최대 메모리 사용량, 프로그램 실행 시간을 의미한다.
예상대로 버퍼의 크기가 작을 수록 시간 소모가 크도 메모리 사용량이 작았으며, 클수록 시간소모는 작고 메모리 사용량이 컸다.
같은 문제를 겪고있는 학부생입니다. 많은 도움이 되었습니다!!!