다음 내용은 책 '데이터베이스를 지탱하는 기술'을 읽고 정리한 글입니다.
소프트웨어 개발 현장에서는 처음에 기획된 것이 바뀌지 않는 경우가 오히려 더 드물다. 따라서 그러한 사양 변경에 신속하게 대응할 수 있는 설계 기술이 중요하다.
데이터베이스를 사용하면 인증, SQL 작성, 결과 셋의 검색 처리 등의 특수 처리가 필요하다.
이러한 번거로운 일을 함녀서까지 DB를 사용하는 이유는 뭘까?
데이터가 증가함에 따라서 시간은 그에 비례하여 엄청나게 증가할 것이다. 따라서 보다 효율적인 데이터 구조로 이를 관리해야 한다.
인덱스 구조의 라이브러리를 사용할 수 있지 않나? 라고 생각했다면 그 구조는 메모리에 의존하게 된다. 이러한 구조는 메모리 내에서밖에 사용할 수 밖에 없다. 또한, 메모리는 휘발성이다.
기업의 입장에서 1분의 서비스 정지는 천문학적인 손해로 돌아온다. 따라서 이에 대한 대책을 철저히 할 필요가 있다.
단순한 백업만으로 생길 수 있는 문제.
웹 서비스를 운영하는 경우에는 여러 사람이 동시에 액세스하는 환경이 당연하다.
배타 제어를 하지 않으면 일관성이 무너지고, 배타 제어의 범위가 넓으면 병렬성이 떨어진다.
대규모 웹사이트에서 접속 빈도가 초당 수천을 넘어가는 상황에는 정보를 얼마나 빨리 반환하는가가 중요한 요소일 것이다.
어떠한 데이터를 찾으려면 파일을 처음부터 탐색하여 찾을 때 까지 검색해야 한다. (선형 검색을 사용한다.)
위에서 언급한 단점들을 대응하고자 나온 것이 B+Tree 인덱스 이다.
정상이 Root 블록, 최하층이 leaf블록 이라고 불리며 그 사이는 branch블록 이라고 불린다.
루트 블록과 브랜치 블록은 사용자 ID에 대해 해당 블록이 어디 있느닞에 대한 정보를 가지고 있다. (인덱스 검색 시에 루트-브랜치-리프 순으로 도달하여 원하는 데이터를 얻을 수 있다.)
브랜치 및 리프의 분기 개수가 두 개밖에 없는 것을 이진트리라고 한다.
모든 값을 리프 블록에서만 갖도록 제한하지 않으며,브랜치에서도 값을 가질 수 있는 데이터 구조이다.
B-Tree에 비해 B+Tree는 루트에서 리프까지 거쳐야 검색을 완료할 수 있지만, 인덱스 자체의 계층 구조를 작게할 수 있다는 장점이 있다.
B-Tree는 리프뿐만 아니라 브랜치에서도 키 정보를 가지고 있어 주변의 리프가 아닌 브랜치로 되돌아가 값의 존재 확인을 실시하고 그 위의 브랜치로 돌아가거나 다른 리프로 가는 등의 처리를 거쳐야 하지만, B+Tree는 리프 블록에서 인접한 리프 블록으로 건너가는 것만으로 값의 탐색이 가능하기에 보다 효율적이다.
즉, 좀 더 유연하다고 할 수 있다.
실제 시장에서는 B+Tree 인덱스의 구성에 더해 실제 용도를 상정하여 다양한 최적화를 진행하고 있다.
인덱스는 고유성을 보증하는 목적으로도 사용할 수 있다.
데이터베이스 서버에서는 고유성을 보장하려는 열에 인덱스를 지정하거나 내부적으로 인덱스를 작성하는 것이 대부분이다.
여러 조건의 인덱스.
대부분의 RDBMS는 (사용자ID, 최종 갱신일)과 같이 두 가지 요소를 결합한 인ㄷ게스를 만들 수 있다. (AND 조건에서 검색을 가속화 할 수 있다.)
인덱스 부분과 데이터 부분은 서로 독립적 영역에 존재하고 있기 때문에 양쪽을 한 번의 액세스로 단번에 읽을 수는 없다. 즉, 데이터를 2번 불러와야 한다.
인덱스 부분만을 불러와도 되는 경우(어떤 조건의 상품 개수를 알고 싶을 때는 단순히 인덱스의 개수를 세면 된다.)에는 인덱스 영역만을 읽어서 검색을 고속화 한다.
한 번의 검색에서 두 개 이상의 인덱스를 동시에 사용하여 각각의 결과에서 원하는 레코드를 꺼내려고 하는 동작.
데이터베이스에서는 무정지성(시스템 내에서 어떠한 장애가 발생하더라도 아무런 데이터의 손실 및 파괴 없이 시스템이 동작해야 한다.)을 확보하기 위해 갱신한 데이터를 디스크에 써야 한다.
메모리에 맞지 않는 규모의 데이터를 취급하는 것이 대부분이며, 메모리에 올라와 있지 않은 데이터를 읽기 위하여 디스크로부터의 로드가 발생한다.
대부분들의 경우 이것들은 랜덤 액세스로, 랜덤 액세스 시간은 HDD 라면 디스크가 반 정도 회전하는 시간이므로 한 초당 수천 회의 수준의 랜덤 액세스 요청이 들어와 버리면 처리를 수행할 수 없게 된다.
인덱스는 본체 데이터와 별도로 유지할 필요가 있기에 검색 성능이 높아지는 대가로 업데이트 성능이 떨어진다.
이 업데이트 비용을 최대한 떨어뜨리기 위해 다양한 최적화를 구현하고 있다.
레코드를 추가 등록하는 겨웅에는 인덱스에도 값을 등록해야 한다.
오름차순으로 등록된다는 보장이 없기에 인덱스의 리프 블록의 이곳저곳이 무작위에 가까운 형태로 업데이트 되어 간다. 랜덤억세스는 느리기에 이 비용을 얼마나 감소시키는가가 성능에 있어서 중요하다.
DB는 무정지성을 높이기 위해 업데이트도니 부분을 최대한 빨리 디스크에 저장해야 한다. 하지만, 업데이트된 정보를 메모리나 전용 파일 등에 일시적으로 기록하여 두고 나중에 모아서 단번에 리프 블록을 갱신하는 구조를 사용한다면 이 성능을 개선할 수 있을 것이다. 그리고 이 구조를 사용하는 DB가 MySQL이다.
B+Tree 인덱스에 값의 추가/갱신/삭제를 할 경우, 인덱스의 리프 블록의 내용을 이동시킬 필요가 있게 된다. (리프 분할).
여러 클라이언트에 일제이 업데이트가 발생하여 리프 분할이 여기저기에서 일어난 경우, 그것들의 일관성을 갖는 것은 상당히 어려운 작업. 따라서 MySQL에서는 인덱스의 재편성 처리가 완료될 때까지 일체의 참조/갱신 처리를 차단하는 동작을 시행한다. 재편성 조작 자체는 금방 끝나지만 동시에 대량의 클라이언트로부터 동일 테이블에 대해 일제히 요청을 시행하여도 동시성이 그다지 높아지지 않는 문제가 있다.
멀티코어 CPU 환경의 대두로 이러한 병렬 갱신 성능의 중요성이 높아졌다.
현재 MySQL에서 이것을 해결하는 가장 빠른 방법은 파티션 테이블을사용하는 것이다. (사용자에게는 테이블이 한 개로 보이자만 내부적으로 복수로 분할 관리되는 것. 인덱스도 복수로 구분하고 있기에 병렬 갱신이 가능하다.)