Database/MySQL

B-TREE 인덱스 1편

Debin 2022. 12. 1.
반응형

2023.10.11 복습 리팩토링 시작

B-Tree 인덱스

  • B-Tree는 제일 범용적인 인덱스 알고리즘이다.
  • 여기서 B는 Binary가 아니라 Balanced라는 의미를 지닌다.
  • B-Tree 칼럼의 원래 값을 변형시키지 않고 (물론 값의 앞부분만 잘라서 관리하기는 하지만) 인덱스 구조체 내에서는 항상 정렬된 상태를 유지한다.
  • 전문 검색과 같은 특수한 요건이 아닌 경우, 대부분의 인덱스는 B-Tree를 사용할 정도로 일반적인 용도에 적합한 알고리즘이다.

구조 및 특성

  • B-Tree는 트리 구조의 최상위에 하나의 루트 노드가 존재하고 그 하위에 자식 노드가 붙어 있는 형태다.
  • 트리 구조의 가장 하위에 있는 노드를 리프 노드라 하고,
  • 트리구조에서 루트 노드도 아니고 리프노드도 아닌 중간의 노드를 브랜치 노드라고 한다.
  • 데이터베이스에서 인덱스와 실제 데이터가 저장된 데이터는 따로 관리되는데, 인덱스의 리프 노드는 항상 실제 데이터 레코드를 찾아가기 위한 주솟 값을 가지고 있다.

 

B-Tree 인덱스의 구조

  • 위 그림처럼 인덱스의 키 값은 모두 정렬돼 있지만, 데이터 파일의 레코드는 정렬돼 있지 않고 임의의 순서로 저장돼 있다.
  • 많은 사람들이 데이터 파일의 레코드는 INSERT된 순서대로 저장되는 것으로 생각하지만 그렇지 않다.
  • 만약 테이블의 레코드를 전혀 삭제하거나 변경하지 않고 INSERT만 수행한다면 맞을 수도 있다.
  • 하지만 레코드가 삭제되어 빈 공간이 생기면 그 다음의 INSERT는 가능한 한 삭제된 공간을 재활용하도록 DBMS가 설계되기 때문에 항상 INSERT된 순서로 저장되는 것은 아니다.

참고

  • InnoDB 테이블에서 레코드는 클러스터되어 디스크에 저장되므로 기본적으로 프라이머리 키 순서로 정렬되어 저장된다.
  • InnoDB에서는 사용자가 별도의 명령이나 옵션을 선택하지 않아도 디폴트로 클러스터링 테이블이 생성된다.
  • 클러스터링이란 비슷한 값을 최대한 모아서 저장하는 방식을 의미한다.

 

인덱스는 테이블의 키 칼럼만 가지고 있으므로 나머지 칼럼을 읽으려면 데이터 파일에서 해당 레코드를 찾아와야 한다.

이를 위해 인덱스의 리프 노드는 데이터 파일에 저장된 레코드의 주소를 가리킨다.

 

MyISAM 테이블의 인덱스 관련 내용은 아래 포스팅에서 확인할 수 있다.

살펴볼 내용은 MyISAM 테이블에 저장되는 레코드는 모두 ROWID라는 물리적인 주솟값을 가지는데,

프라이머리 키와 세컨더리 인덱스는 모두 데이터 파일에 저장된 레코드의 ROWID 값을 포인터로 가진다는 것이다.

 

https://devdebin.tistory.com/250

 

MyISAM 스토리지 엔진 아키텍처

MyISAM 스토리지 엔진 아키텍처 MyISAM 스토리지 엔진의 성능에 영향을 미치는 요소인 키 캐시와 운영체제의 캐시/버퍼에 대해 살펴보자. 키 캐시 InnoDB 버퍼 풀과 비슷한 역할을 하는 것이 MyISAM의

devdebin.tistory.com

 

InnoDB 테이블에서는 프라이머리 키가 ROWID 역할을 한다.

MyISAM과 InnoDB 스토리지 엔진에서 가장 큰 차이점은 세컨터리 인덱스를 통해 데이터  파일의 레코드를 찾아가는 방법에 있다.

 

MyISAM 테이블은 세컨더리 인덱스가 물리적인 주소를 가지는 반면,

InnoDB 프라이머리 키를 주소처럼 사용하기 때문에 논리적인 주소를 가진다고 볼 수 있다.

 

그래서 InnoDB 테이블에서 인덱스를 통해 레코드를 읽을 때는 데이터 파일을 바로 찾아가지 못한다.

 

인덱스에 저장돼 있는 프라이머리 키 값을 이용해 프라이머리 키 인덱스를 한 번 검색한 후,

프라이머리 키 인덱스의 리프 페이지에 저장돼 있는 레코드를 읽는다.

 

즉, InnoDB 스토리지 엔진에서는 모든 세컨더리 인덱스 검색에서 데이터 레코드를 읽기 위해서는 반드시 프라이머리 키를 저장하고 있는

B-Tree를 다시 한 번 검색해야 한다.

이 같은 과정으로 인해 InnoDB 테이블의 성능이 떨어질 것처럼 보이지만, 사실은 아니다. 이는 추후에 더 살펴보겠다.

B-Tree 인덱스 키 추가 및 삭제

인덱스 키 추가

  • B-Tree에 저장될 때는 저장될 키 값을 이용해 B-Tree 상의 적절한 위치를 검색해야 한다.
  • 저장될 위치가 결정되면 키 값과 대상 레코드의 주소 정보를 B-Tree의 리프 노드에 저장한다.
  • 리프 노드가 꽉 차서 더는 저장할 수 없을 때는 리프 노드가 분리돼야 하는데, 이는 상위 브랜치 노드까지 처리의 범위가 넓어진다.
  • 이런 작업 때문에 B-Tree는 상대적으로 쓰기작업에 비용이 많이 드는 것으로 알려져 있다.
  • 인덱스 추가 비용은 테이블에 레코드를 추가하는 작업 비용을 1이라고 가정하면
  • 해당 테이블의 인덱스에 키를 추가하는 작업 비용을 1.5 정도로 예측하면 된다.
  • MyISAM이나 MEMORY 스토리지 엔진을 사용하는 테이블에서는 INSERT 문장이 실행되면 즉시 새로운 키 값을 B-Tree 인덱스에 변경한다.
  • 하지만 InnoDB 스토리지 엔진은 이 작업을 조금 더 지능적으로 처리하는데, 필요하다면 인덱스 키 추가 작업을 지연시켜 나중에 처리할 수 있다.
  • 하지만 프라이머리 키나 유니크 인덱스의 경우 중복 체크가 필요하기 때문에 즉시 B-Tree에 추가하거나 삭제한다.

인덱스 키 삭제

  • B-Tree의 키 값이 삭제되는 경우는 해당 키 값이 저장된 B-Tree의 리프 노드를 찾아서 그냥 삭제 마크만 하면 작업이 완료된다.
  • 이렇게 삭제 마킹된 인덱스 키 공간은 계속 그대로 방치하거나 재활용 할 수 있다.
  • InnoDB 스토리지 엔진에서는 버퍼링되어 지연 처리될 수 있고, MyISAM이나 MEMORY 스토리지 엔진의 테이블에서는 체인지 버퍼와 같은 기능이 없으므로 인덱스 키 삭제가 완료된 후 쿼리 실행이 완료된다.

인덱스 키 변경

  • 인덱스의 키 값은 그 값에 따라 저장될 리프 노드의 위치가 결정되므로 B-Tree의 키 값이 변경되는 경우에는 단순히 인덱스상의 키 값만 변경하는 것은 불가능하다.
  • B-Tree의 키 값 변경 작업은 먼저 키 값을 삭제한 후, 다시 새로운 키 값을 추가하는 형태로 처리된다.
  • 인덱스 키 값을 변경하는 작업은 기존 인덱스 키 값을 삭제한 후 새로운 인덱스 키 값을 추가하는 작업으로 처리되고, InnoDB 스토리지 엔진을 사용하는 테이블에 대해서는 이 작업 모두 체인지 버퍼를 활용해 지연 처리 될 수 있다.

인덱스 키 검색

  • INSERT, UPDATE, DELETE 작업을 할 때 인덱스 관리에 따르는 추가 비용을 감당하면서 인덱스를 구축하는 이유는 빠른 검색을 위해서다.
  • 인덱스를 검색하는 작업은 B-Tree의 루트 노드부터 시작해 브랜치 노드를 거쳐 최종 리프 노드까지 이동하면서 비교 작업을 수행하는데, 이 과정을 '트리 탐색'이라 부른다.
  • B-Tree 인덱스를 이용한 검색은 100% 일치 또는 값의 앞부분만 일치하는 경우에 사용할 수 있다.
  • 부등호 비교 조건에서도 인덱스를 활용할 수 있지만, 인덱스를 구성하는 키 값의 뒷부분만 검색하는 용도로는 인덱스를 사용할 수 없다.
  • 또한 인덱스를 이용한 검색에서 중요한 사실은 인덱스의 키 값에 변형이 가해진 후 비교되는 경우에는 B-Tree의 빠른 검색 기능을 사용할 수 없다는 것이다.
  • 이미 변형된 값은 B-Tree 인덱스에 존재하는 값이 아니다. 따라서 함수나 연산을 수행한 결과로 정렬한다거나 검색하는 작업은 B-Tree의 장점을 이용할 수 없으므로 주의해야 한다.
  • InnoDB 테이블에서 지원하는 레코드 잠금이나 넥스트 키락이 검색을 수행한 인덱스를 잠근 후 테이블의 레코드를 잠그는 방식으로 구현돼 있다.
  • 따라서 UPDATE나 DELETE 문장일 실행될 때 적절히 사용할 수 있는 인덱스가 없으면 불필요하게 많은 레코드를 잠근다.
  • 심지어 모든 테이블의 모든 레코드를 잠글 수도 있다.
  • InnoDB 스토리지 엔진에서는 인덱스의 설계가 중요하고 많은 부분에 영향을 미친다.

B-Tree 인덱스 사용에 영향을 미치는 요소

B-Tree 인덱스 사용에 영향을 미치는 요소는 아래와 같이 있다.

인덱스 키 값의 크기

  • InnoDB 스토리지 엔진은 디스크에 데이터를 저장하는 가장 기본 단위를 페이지 또는 블록이라고 하며, 디스크의 모든 읽기 및 쓰기 작업의 최소 단위가 된다.
  • 또한 페이지는 InnoDB 스토리지 엔진의 버퍼 풀에서 데이터를 버퍼링하는 기본 단위이기도하다.
  • 결국은 페이지 단위로 관리되며, 루트와 브랜치 그리고 리프 노드를 구분한 기준이 바로 페이지 단위다.
  • 결론을 말하면 인덱스를 구성하는 키 값의 크기가 커지면 디스크로부터 읽어야 하는 횟수가 늘어나고, 그만큼 느려진다는 것을 의미한다.
  • 또한 인덱스의 키 값의 길이가 길어진다는 것은 전체적인 인덱스의 크기가 커진다는 것을 의미한다.
  • 하지만 인덱스를 캐시해 두는 InnoDB 버퍼 풀이나 MyISAM의 키 캐시 영역은 크기가 제한적이기 때문에 하나의 레코드를 위한 인덱스 크기가 커지면 커질수록 메모리에 캐시해 둘 수 있는 레코드 수는 줄어든다.
  • 그렇게 되면 자연히 메모리의 효율이 떨어지는 결과를 가져온다.

B-Tree 깊이

  • B-Tree 인덱스의 깊이는 상당히 중요하지만 직접 제어할 방법은 없다.
  • B-Tree의 깊이는 MySQL에서 값을 검색할 때 몇 번이나 랜덤하게 디스크를 읽어야 하는지와 직결되는 문제다.
  • 결론적으로 인덱스 키 값의 크기가 커지면 커질수록 하나의 인덱스 페이지가 담을 수 있는 인덱스 키 값의 개수가 적어지고,
  • 그 때문에 같은 레코드 건수라고 하더라도 B-Tree의 깊이가 깊어져서 디스크 읽기가 더 많이 필요하게 된다는 것을 의미한다.
  • 위 내용은 사실 인덱스 키 값의 크기는 가능하면 작게 만드는 것이 좋다는 것을 강조하기 위함이다.
  • 아무리 대용량 DB라도 B-Tree 깊이가 5단계 이상까지 깊어지는 경우는 흔치 않다.

선택도(기수성)

  • 인덱스에서 선택도 또는 기수성은 거의 같은 의미이며, 모든 인덱스 키 값 가운데 유니크한 값의 수를 의미한다.
  • 전체 인덱스 키 값은 10개인데, 그중에서 유니크한 값의 수는 10개라면 기수성은 10이다.
  • 인덱스 키 값 가운데 중복된 값이 많아지면 많아질수록 기수성은 낮아지고 동시에 선택도 또한 떨어진다.
  • 인덱스는 선택도가 높을수록 검색 대상이 줄기 때문에 그만큼 빠르게 처리된다.

읽어야 하는 레코드 건수

  • 인덱스를 통해 테이블의 레코드를 읽는 것은 인덱스를 거치지 않고 바로 테이블의 레코드를 읽는 것보다 높은 비용이 드는 작업이다.
  • 인덱스를 이용한 읽기의 손익 분기점이 얼마인지 판단할 필요가 있는데, 일반적인 DBMS의 옵티마이저에서는 인덱스를 통해 레코드 1건을 읽는 것이 테이블에서 직접 레코드 1건을 읽는 것보다 4 ~ 5배 정도 비용이 더 많이 드는 작업인 것으로 예측한다.
  • 즉 인덱스를 통해 읽어야 할 레코드의 건수가 전체 테이블 레코드의 20 ~ 25%를 넘어서면 인덱스를 이용하지 않고 테이블을 모두 직접 읽어서 필요한 레코드만 가려내는 방식으로 처리하는 것이 효율적이다.

이상으로 포스팅을 마칩니다. 감사합니다.

 

참고

REAL MySQL 8.0 1권

반응형

댓글