Post

<오라클 성능 고도화 원리와 해법2> Ch01-09 비트맵 인덱스

오라클 성능 고도화 원리와 해법2 - Ch01-09 비트맵 인덱스

인덱스는 키값에 해당하는 테이블 레코드를 찾아갈 수 있도록 주소 정보를 제공한다. 일반적으로 사용되는 B*Tree 인덱스는 테이블 레코드를 가리키는 rowid 목록을 키값과 함께 저장하는 구조다. 테이블에 100개 레코드가 있으면 인덱스에도 100개 rowid를 키값과 함께 저장한다. rowid에는 중복값이 없지만 키에는 중복값이 있을 수 있다.

개념적으로 볼 때22), 비트맵 인덱스는 키값에 중복이 없고, 키값 별로 하나의 비트맵 레코드를 갖는다. 그리고 비트맵 상의 각 비트가 하나의 테이블 레코드와 매핑된다. 비트가 1로 설정돼 있으면 상응하는 테이블 레코드가 해당 키값을 포함하고 있음을 의미한다.

  1. 테이블 로우 수가 아주 많다면, 실제로는 키값 별로 레코드가 여러 개일 수 있다.

(1) 비트맵 인덱스 기본 구조

그림1-58을 보면 비트맵 인덱스 구조를 쉽게 이해할 수 있다. 그림처럼 상품 테이블에 10개 레코드가 있고, 색상으로는 BBD, GREEN, BLUE가 입력돼 있다고 하자. 8번 상품에는 색상이 입력되지 않았다.

그림 아래쪽은 색상 컬럼에 생성한 비트맵 인덱스를 표현한 것인데, 키값이 BLUE인 첫 번째 행을 보면 4번째, 7번째, 9번째 비트가 1로 설정돼 있다. 따라서 상응하는 테이블 레코드의 색상 값이 BLUE임을 뜻한다.

블록 덤프를 통해 실제 비트맵 인덱스 내부 구조를 살펴보면, 아래와 같은 정보를 담고 있다.

마지막 컬럼(col3)은 시작 rowid와 종료 rowid 구간에 속한 테이블 레코드와 매핑되는 비트맵이다. 첫 번째 비트는 시작 rowid가 가리키는 레코드와 매핑되고, 마지막 비트는 종료 rowid가 가리키는 레코드와 매핑된다. 그림1-59를 참조한다면 쉽게 이해할 것이다.

비트맵 인덱스는 이처럼 첫 번째와 마지막 비트의 rowid만을 갖고 있다가 테이블 액세스가 필요할 때면 각 비트가 첫 번째 비트로부터 떨어져 있는 상대적인 거리를 이용해 rowid 값을 환산한다.

비트맵 위치와 rowid 매핑

여기서 의문점이 생긴다. 데이터 블록은 한 익스텐트 내에서 연속된 상태로 저장되지만 익스텐트끼리는 서로 인접해 있지 않다. 심지어 다른 데이터 파일에 흩어져 저장되는데, 어떻게 시작 rowid와의 상대적인 거리로 정확한 레코드 위치를 알아낼 수 있을까?

오라클이 한 블록에 저장할 수 있는 최대 레코드 개수를 제한한다는 데서 힌트를 얻을 수 있다.

확실히 오라클은 한 블록에 저장할 수 있는 최대 레코드 개수를 제한하고 있다. 이제 이 특징을 이용해 오라클이 어떻게 비트맵 위치와 rowid를 매핑하는지 살펴보자.

예를 들어, 그림1-58에서 본 상품 테이블에 총 20개 블록이 할당되었다고 하자. 첫 번째와 두 번째 익스텐트에 각각 10개 블록이 있고, 하나의 테이블 블록이 가질 수 있는 최대 레코드 개수는 730이라고 하자.

이 테이블 색상 컬럼에 비트맵 인덱스를 만들면, 오라클은 네 개(BLUE, GREEN, RED, NULL)의 키값에 각각 14,600=730x20)개 비트를 할당(초기값은)하고 값에 따라 비트를 설정한다. 첫 번째 키 값을 예로 들면 색상이 ‘BLUE’인 레코드에 해당하는 비트를 모두 1로 설정한다.

비트맵 인덱스를 스캔하면서 테이블 레코드를 찾아갈 때는? 예를 들어, 9,500번째 비트가 1로 설정돼 있으면 14번째 블록(두 번째 익스텐트 4번째 블록) 10번째 레코드를 찾아가면 된다. 아래 계산식을 참고하기 바란다.

1
2
floor(9500/730) = 13 -> 14번째 블록
mod(9500, 730) = 10 -> 10번째 레코드

반대로, 14번째 블록 10번째 레코드의 비트를 1로 설정하려면 아래 공식으로 해당 비트를 찾아간다.

1
730 × 31 + 10 = 9,500

키 값의 수가 많을 때

비트맵 인덱스는 키 값별로 하나의 레코드를 갖는데, 저장할 키 값의 수가 아주 많을 때는 한 블록에 모두 담지 못한다. 비트맵을 저장하기 위해 두 개 이상 블록이 필요해지면 오라클은 그림1-60처럼 B*Tree 인덱스 구조를 사용하며, 값의 수가 많을 수록 인덱스 높이도 증가한다.

이런 구조라면 B*Tree 인덱스보다 더 많은 공간을 차지할 수 있어 비트맵 인덱스로는 부적합하다.

키 값별로 로우 수가 많을 때

한 블록 크기의 비트맵으로 표현할 수 없을 정도로 테이블 로우 수가 많을 때도 두 개 이상 블록이 필요해진다. 예를 들어, BLUE 값 하나에 대한 비트맵을 저장하기 위해 2+1/2 블록만큼의 공간을 필요로 한다면 그림 1-61과 같은 구조로 저장된다.

그림1-61처럼 저장한다면 한 블록이 단 하나의 비트맵 레코드로 구성될 수 있지만 실제 테스트 해보면, 한 블록에 적어도 2개 비트맵 레코드가 담기도록 잘라서 저장하는 것을 확인할 수 있다. 예를 들어, 그림1-61맨 왼쪽 리프 블록에는 BLUE에 대한 두 개의 비트맵 레코드가 저장된다.

비트맵 압축

그림1-59와 그림1-60을 보면, 모든 비트맵의 시작 rowid와 종료 rowid를 같게 표현하였다. 그것은 비트맵을 압축하지 않았을 때의 모습이고, 실제로는 여러 가지 압축 알고리즘이 사용되기 때문에 서로 다른 rowid 범위를 갖는다.

그림 1-61 처럼 테이블 로우 수가 아주 많고 그림 1-60 처럼 키 값의 수도 많다면 완전히 0으로 채워진 비트맵 블록들이 생기는데, 오라클은 그런 블록들을 제거한다. 비트맵 뒤쪽에 0이 반복되어도 이를 제거한다. 그리고 앞, 뒤, 중간 어디든 같은 비트맵 문자열이 반복되면 checksum 비트를 두어 압축한다.

이 때문에 각 비트맵이 가리키는 rowid 구간이 서로 달라지지만 시작 rowid와 종료 rowid만 알고 있으면 비트와 매핑되는 rowid를 계산하거나 다른 비트맵과 Bitwise 연산하는 데에는 전혀 지장이 없다.

(2) 비트맵인덱스활용

비트맵 인덱스는 성별처럼 Distinct Value 개수가 적을 때 저장 효율이 매우 좋다. 그런 컬럼이라면 B*Tree 인덱스보다 훨씬 적은 용량을 차지하므로 인덱스가 여러 개 필요한 대용량 테이블에 유용하다. 주로 다양한 분석 관점을 가진 팩트성 테이블이 여기에 속한다. 반대로 Distinct Value가 아주 많은 컬럼이면 오히려 B*Tree 인덱스보다 많은 공간을 차지한다(그림1-60 참조).

Distinct Value 개수가 적은 컬럼일 때 저장 효율이 좋지만 테이블 Random 액세스 발생 측면에서는 B*Tree 인덱스와 똑같기 때문에 그런 컬럼을 비트맵 인덱스로 검색하면 그다지 좋은 성능을 기대하기 어렵다. 스캔할 인덱스 블록이 줄어드는 정도의 성능 향상만 얻을 수 있다.

하나의 비트맵 인덱스 단독으로는 쓰임새가 별로 없지만 여러 비트맵 인덱스를 동시에 사용할 수 있다는 특징 때문에 대용량 데이터 검색 성능을 향상시키는데 큰 효과를 발휘한다. 예컨대, 여러 개 비트맵 인덱스로 Bitwise 연산을 수행한 결과 테이블 액세스량이 크게 줄어든다면 극적인 성능 향상을 가져다준다.

두 개 이상 비트맵을 이용한 Bitwise AND 연산뿐만 아니라 Bitwise OR, Bitwise Not 연산도 가능하다.

비트맵 인덱스는 여러 인덱스를 동시에 활용할 수 있다는 장점 때문에 다양한 조건절이 사용되는, 특히 정형화 되지 않은 임의 질의( ad-hoc query)가 많은 환경에 적합하다.

다만, 비트맵 인덱스는 lock에 의한 DML 부하가 심한 것이 단점이다. 레코드 하나만 변경되더라도 해당 비트맵 범위에 속한 모든 레코드에 lock이 걸린다. OLTP 성환경에 비트맵 인덱스를 쓸 수 없는 이유가 여기에 있다!

지금까지 설명한 특징을 고려할 때 비트맵 인덱스는 읽기 위주의 대용량 DW(특히, OLAP) 환경에 아주 적합하다.

(3) RECORDS_PER_BLOCK

오라클은 한 블록에 저장할 수 있는 최대 레코드 개수를 제한한다고 했다. 그래야 비트맵 위치와 rowid를 매핑할 수 있기 때문이다.

그런데 실제 블록에 저장되는 평균적인 레코드 개수는 여기에 한참 못 미치므로 비트맵 인덱스에 낭비되는 공간이 많이 생긴다. 이에 오라클은 블록에 저장될 수 있는 최대 레코드 개수를 사용자가 지정할 수 있는 기능을 제공한다.

생각만큼 큰 폭으로 줄지 않은 이유는, ‘오라클이 처음에 정한 블록 당 최대 크기’를 기준으로 비트를 할당할 때도 내부적으로 사용한 압축 알고리즘에 의해 크기가 많이 줄기 때문이다.

주의할 것은, 블록 당 레코드 개수가 정상치보다 낮은 상태에서 minimize records per block 명령을 수행하지 말아야 한다는 사실이다. 그럴 경우, 추가로 데이터가 입력되면서 블록마다 공간이 많이 생기고, 비트맵 인덱스가 줄어드는 것 이상으로 테이블 크기가 커지는 결과를 초래한다.

This post is licensed under CC BY 4.0 by the author.