스프링 프로젝트를 하면서 최적화에 대한 문제를 여러 번 직면했다. api 횟수를 줄이는 문제, DB와의 통신 횟수를 줄이는 방식, N +1 문제, JPA 읽기 모드, 캐시, DB 검색 성능 향상(인덱스 및 join성능 향상 등), 정규화, 테이블 설계, 저장 등 매우 많은 방식이 존재한다는 것을 알았다. 그중에서 DB에 데이터를 저장하거나 검색을 할 때의 성능을 향상하기 위해 새로 쓴 대용량 데이터베이스 설루션 1이라는 책을 읽고 책의 내용을 정리하려고 한다.
일반적으로 인덱스와 테이블은 분리되어 있다. 테이블은 테이블 스페이스에 여러 블록에 저장되어 있고 각 로우(Row)들의 주소가 인덱스 키에 따라 테이블에서 지정한 칼럼들의 순서로 인덱스가 만들어지는 것이다. 여기서 문제는 각각의 로우는 여러 블록에 랜덤으로 흩어져 있다는 것이다. 인덱스로 설정한 칼럼이 어느 컬럼인지에 상관없이 각각의 로우는 여러 블록에 흩어져 있는 것이다. 인덱스에 있는 로우의 주소(ROWID)를 통해 로우가 있는 블록에 접근을 하는 것이다.
예를 들어보면 8개의 블록 중 3개의 블록에 찾고자 하는 로우들이 모여있는 경우와 8개의 블록 모두에 고르게 흩어져 있는 경우중 어느 것이 더 비용이 많이 들겠는가?
당연히 후자의 경우가 비용이 많이 들 것이다.
이렇듯 인덱스의 순서와 찾고자 하는 테이블의 로우들이 비슷하게 모여 있는 것을 클러스터링 팩터라고 한다.
클러스터링 팩터가 좋을수록 성능이 좋다고 할 수 있다. 클러스터링 팩터가 좋을수록 적은 블록을 액세스 하게 되면서 효율이 높아지는 것이다.
인덱스를 따라서 테이블의 로우를 찾는 것을 랜덤 액세스라고 하는데 이러한 랜덤 액세스를 줄이기 위한 방식은 여러 가지가 있다.
첫 번째는 테이블 재생성이다. 테이블 재생성을 설명하기 전에 먼저 응집도와 체인에 대해 간단하게 설명을 해보겠다. 먼저 체인(Chain)에 대해서 설명해 보겠다. 데이터를 수정할 때 이미 발생된 로우들의 길이가 너무 많이 커져서 자동으로 로우들을 재구성하는 응축작업이 일어나게 된다. 하지만 아무리 여유공간을 충분하게 부여했더라도 이미 발생한 로우들의 길이가 너무 많이 커져 응축을 하더라도 로우의 길이가 늘어날 때 사용할 여유공간이 부족하게 되었다면 더 이상 응축을 할 수 없는 상황이 올 것이다. 이러한 경우에는 결국 부족한 공간을 다른 블록으로 메울 수밖에 없다. 그러나 이로 인해 로우가 다른 블록으로 이동했다면 이것은 곧 로우의 주소(ROWID)의 변경을 의미한다. 그러나 이러한 경우에도 ROWID를 변경시키지 않도록 하는 방법이 있다. 그것은 과거의 주소로 나를 찾아왔을 때 거기에 옮긴 주소를 넣어두는 방법이다. 이 방식은 인덱스의 ROWID를 변경시키지 않는 대신에 액세스를 할 때 여러 블록을 읽어야 하는 오버헤드를 감수해야 한다. 이를 '로우의 이주'라고 한다.
이와 유사한 개념이 바로 체인이다. 이는 여러 블록에 걸쳐 데이터가 존재한다는 점에서 이주와 유사하지만 본질적인 차이가 있다. 만약 어느 로우의 길이가 한 블록을 넘는다면 하나의 블록 안에 다 집어넣을 수는 없다. 이때 필요한 공간만큼 블록을 연결해서 저장해야 하는데 이것을 '체인이 발생했다'라고 한다.
일반적으로 로우는 가변 길이이기에 NULL값인 로우에 나중에 값이 들어오는 등 길이가 달라진다는 것은 한 조각으로 블록 내에 있기 위해 위치 이동이 발생할 수 있으며 이주나 체인 방식으로 DBMS는 해결을 할 수 있는 것이다.
그렇다면 테이블을 재생성한다면 어떤 일이 발생할까? 일반적으로 로우들이 수정되면서 응축이나 체인이 발생한 테이블은 로우들이 여러 블록에 흩어지면서 당연히 클러스터링 팩터가 안 좋을 것이다. 이러한 테이블을 재생성하게 되면 응집도가 높아지고 체인이 줄어들면서 블록 내 데이터 저장률은 높아지게 된다.
물론 테이블을 재생성하는 데는 부하가 발생할 것이다. 그러나 이러한 부담은 DBMS의 성능이 크게 향상되면서 시스템에 그리 심각한 부담을 주지는 않는다. 그렇기에 주기적으로 테이블을 재 생성하는 것은 좋은 방법이 될 것이다.
두 번째 방식은 인덱스 일체형 테이블을 사용하는 것이다. 우리가 주로 사용하는 인덱스 분리형 테이블이 가지는 최대의 특징은 데이터의 값에 전혀 무관하게 '임의의 위치'에 저장된다는 것이다.
하지만 인덱스와 테이블이 일체형으로 되어 있다는 것은 인덱스와 다른 일반 칼럼들이 모두 같은 위치에 저장되는 형태를 말한다. 그러므로 인덱스만 액세스 하면 따로 테이블을 액세스 할 필요는 없다.
분리형 구조에서는 인덱스를 경유하는 액세스를 할 때 처리 범위가 넓어지면 부담이 크게 증가한다. 가장 큰 이유는 인덱스 스캔이 아니라 테이블을 찾는 랜덤 액세스 때문이다. 인댁스는 최초 시작 부분만 랜덤 액세스를 하고 종료지점까지 스캔하기 때문에 범위가 넓어져도 상대적으로 부담이 적다. 그러나 ROWID를 이용해 임의의 위치에 있는 테이블의 로우를 액세스 하는 일은 최악을 경우 한 건을 액세스 하기 위해 매번 새로운 블록을 액세스 할 수도 있는 것이다. ROWID를 통해 블록에 액세스하고 로우를 찾아갔지만 로우의 이주가 발하여 다른 블록을 갈 수도 있기 때문이다.
아래는 분리형과 일체형의 차이를 간단히 작성한 표이므로 참고하길 바란다.
이렇게 보면 인덱스 일체형 테이블은 매우 좋아 보인다. 하지만 일체형 테이블은 특정 칼럼에만 집중되어 있고 다른 칼럼의 순서로 정렬을 위해서는 새로운 인덱스 일체형 테이블을 생성해야 하는 큰 단점이 있다. 그 밖에도 여러 단점이 있고 아래의 경우에서만 사용한다.
- 전자 카탈로그나 키워드 검색용 테이블
- 코드성 테이블
- 색인 테이블
- 공간 정보 관리용 테이블
- 대부분 기본키로 검색되는 테이블
- OLAP의 디멘젼 테이블
- 로우의 길이가 비교적 짧고, 트랜젝션이 빈번하게 발생되지 않는 테이블
일반적으로 대부분의 경우에는 인덱스 분리형 테이블을 사용하고 있다.
세 번째 방법은 클러스터링을 사용하는 방법이다.
클러스터는 테이블이나 인덱스처럼 저장공간을 가지고 있는 하나의 오브젝트이다. 그러나 테이블이 인덱스보다 상위 개념이듯이 클러스터는 테이블의 상위 개념이다. 테이블당 하나의 클러스터에 속할 수 있으며 단순하게 생각하면 그룹이라고 생각하면 된다. 여러 테이블이 하나의 클러스터라는 그룹에 속한다는 느낌이다. 클러스터 인덱스 키에 따라 여러 클러스터를 연결하고 클러스터 안에는 여러 로우들이 들어있다. 여러 로우에 접근할 때 클러스터 인덱스 키로 어떤 클러스터를 들어가면 그 클러스터에 여러 개의 로우들을 한 번에 스캔하기에 훨씬 효율적으로 접근할 수 있다.
mysql의 경우에는 클러스터형 인덱스라고 PK를 기준으로 클러스터 키 칼럼으로 정하고 클러스터링을 형성할 수 있게 되어 있다. 테이블당 하나씩 생성할 수 있다. 물론 다른 보조 인덱스들은 테이블당 여러 개를 생성할 수 있는 것이다.
클러스터링은 단일 테이블 클러스터링과 다중 클러스터링으로 구분할 수 있다. 단일 클러스터링은 하나의 테이블로만 테이블을 구성하는 것인데 위에서 언급한 것처럼 클러스터 키가 정렬된 인덱스로 단위 클러스터를 접근해서 여러 개의 로우들을 한 번에 스캔하여 효율적으로 액세스 할 수 있다.
다중 클러스터링은 단위 클러스터에 두 개 이상의 테이블들을 함께 저장하는 것이다. 같은 클러스터 키 칼럼 값을 각진 각 테이블의 로우는 정해진 장소에 같이 저장되므로 테이블을 조인하는 속도를 향상할 수가 있다. 이렇게 두 개 이상의 테이블을 같은 클러스터 내에 저장하더라도 각 테이블의 독립성은 영향을 받지 않는다. 각각의 테이블은 별도로 액세스 할 수 있음은 물론이고, 각 테이블은 자신의 고유한 인덱스를 생성할 수 있기 때문이다.
다중 클러스터링 내의 두 개의 테이블을 조인할 때, 클러스터 인덱스를 경유하여 클러스터를 읽고 같은 클러스터 내에서 연결 작업을 수행한다. 조인할 대상이 같은 클러스터 내애 있다는 것은 추가적인 액세스를 하지 않으므로 매우 효율적인 조인이 가능하다.
클러스터링은 관계형 데이터베이스의 최대 약점인 넓은 범위의 처리를 해결해주고 조인의 효율성을 높여주는 등 장점들을 가지고 있다. 하지만 클러스터링의 단점 또한 존재한다.
먼저 클러스터링의 도입으로 인한 부하가 있다.
그리고 클러스터링 테이블에 입력, 수정, 삭제 시의 부하도 존재한다.
먼저 입력 시의 부하에 대해서 설명해 보겠다. 클러스터링 테이블에 데이터를 입력하게 되면 클러스터링 키에 따라 정해진 위치를 찾아서 저장이 되는데 이로 인해 추가적인 부하가 생길 수밖에 없다. 예를 들어 일반 테이블이 100개의 로우를 한 번에 입력할 때에는 그냥 한꺼번에 집어넣으면 된다. 하지만 1 클러스터링 테이블에서는 100개의 로우가 각각에 해당되는 클러스터링 키값에 따라 따로 저장될 수 있기에 훨씬 부하가 생기고 마는 것이다. 물론 수십, 수백 개 이내의 로우를 처리하는 경우는 큰 문제가 되지는 안지만 수천 개 이상의 데이터를 동시에 입력하는 경우는 주의하여야 한다.
클러스터링 테이블에 있는 칼럼 값을 수정하는 경우에는 두 가지 부하가 있는데 첫 번째는 클러스터링 칼럼 값이 변경되는 것이다. 이때의 부하는 인덱스에서 이주가 발생한 것처럼 클러스터링 키 값에 해당하는 새로운 단위 클러스터의 주소가 이전 단위 클러스터에 로우에 나타나 지게 되면서 연속적인 액세스로 인한 부하가 생기게 되는 것이다.
다른 한 가지 부하는 일반 칼럼을 변경하는 것인데 이것은 일반 테이블에서의 부하와 똑같기에 그리 신경을 쓰지 않아도 된다.
삭제 시의 부하에 대해 설명하자면 우선 로우의 삭제에 대해서는 큰 부담이 아니다. 문제는 테이블의 삭제 시에 발생한다. 일반 테이블의 삭제는 로우들을 일일이 찾아서 삭제시키는 것이 아니라 자료 사전(Data Dictionary)의 정보를 삭제시키고 할당된 저장공간을 해제시키기만 하므로 아주 빠르다. 하지만 클러스터링 테이블은 단위 클러스터가 레코드이고, 로우는 칼럼의 개념이므로 테이블을 삭제하면 DROP가 아닌 DELETE가 발생하게 된다. DROP는 DDL이지만 DELETE는 DML이기에 롤백 세그먼트를 할당받아 더 부하가 생기는 것이다.
예를 들면 다중 클러스터링 테이블 같은 경우 하나의 테이블을 삭제한다고 해서 다른 테이블이 클러스터 안에서 사라지는 것은 아닌 것을 볼 수 있다. 그렇기에 테이블을 삭제한다는 것보단 DROP나 TRUNCATE 명령을 사용하면 된다.
예시 코드는 아래와 같다.
DROP CLUSTER cluster_name
INCLUDING TABLES
CASCADE CONSTRAINTS;
TRUNCAE CLUSTER cluster_name REUSE STORAGE;
위의 CASCADE CONSTRAINTS는 테이블에 제약조건이 지정되어 있다면 추가해야 한다. TRUNCATE는 DELETE처럼 로우만 삭제하지만 수행 속도는 DROP와 동일하다. 그렇기에 클러스터링 테이블을 재생성할 때 현재의 저장공간을 그대로 확보하고자 하거나 각종 자료 사전 정보를 그대로 유지하고자 한다면 TRUNCATE를 사용하는 것이 더 유리하다.
참고
https://jie0025.tistory.com/m/107
https://pearlluck.tistory.com/m/54
새로 쓴 대용량 데이터베이스 설루션 - 이화식
'DataBase' 카테고리의 다른 글
데이터베이스 옵티마이저에 대한 간단 설명 (0) | 2022.04.18 |
---|---|
DB - 결합인덱스 및 컬럼 순서 결정 방법 (4) | 2022.04.10 |
인덱스 간단 정리(개념, 인덱스컬럼 결정) (0) | 2022.04.09 |
인덱스의 유형과 특징(2. 함수기반 인덱스) (0) | 2022.04.02 |
인덱스의 유형과 특징(1. B-Tree index) (0) | 2022.04.01 |