B+ tree Index

B+ tree

B+-Tree Index Files

  • 트리 구조는 검색에 유용하다 왜냐하면 조건에 맞는 노드를 찾아갈 때 필요없는 것을 순식간에 자를 수 있기 때문이다.(잘라내기=pruning 이라고 한다) 이 때문에 구글, 네이버 등 검색 사이트들에서는 이러한 트리의 색인을 이용해 검색 알고리즘에 사용한다.
  • 트리니까 당연히 루트가 있다. rooted tree
    • b-tree는 balanced 트리로 다양한 변형이 존재한다. 그 중 b+ tree가 구조상 삽입,삭제가 쉽기 때문에 가장 많이 사용한다.
      • balanced 트리는 루트에서 leaves 노드들 까지의 깊이(길이)가 같은 트리를 말한다. balanced 트리가 성능이 가장 좋다.

        • unbalanced 트리같은 경우, 한쪽으로 치우쳐 있을 경우에는 검색 성능이 굉장히 나빠진다. 데이터가 어디에 위치하느냐에 따라 검색 속도가 극단적으로 차이가 나게 된다.
      • 구글, 네이버와 같이 데이터도 굉장히 많고 동시 엑세스 사용자도 굉장히 많다. 따라서 balanced 트리가 되어야 검색하는 노드의 갯수가 줄어들기 때문에 성능이 좋아진다.

      • B+트리의 노드는 n개의 포인터와 n - 1개의 키값을 가질 수 있다.

        • $P_i$ : 포인터 (child node 또는 record를 가리키는 포인터)
        • $K_i$ : 키 값 (search-key value)
        • $P$와 $K$는 쌍으로 움직이고 $P_n$이 마지막에 홀로 남아있는 형태이다.
        • 따라서 포인터의 갯수가 키 값인 $K$보다 하나 더 많다.
        • 키 값은 알파벳 순으로, 사전 순으로, 또는 숫자가 작은 순으로 정렬되어 들어가있다.
          • $K_1$ < $K_2$ < $K_3$ < . . . < $K_{n–1}$
        • 이 때 이 n은 fanout 또는 blocking factor라고 부른다. 그리고 이 fanout값 n은 특정한 트리에서 고정되어있다.
          • 예를 들어 $P$는 포인터니까 $P$를 표현하는데 4byte를 쓴다고 가정
          • 키값은 10byte라고 가정
          • 위 두 개는 쌍으로 움직이므로 두 개를 합하면 14byte
          • 한 노드 사이즈를 1KB라고 가정. 즉 1024byte
          • 그러면 위 쌍은 1KB에 73개가 들어간다. 즉 $n-1$이 73이 되고 마지막에 포인터가 하나 더 들어가게 되므로 $n$은 74가 된다.
          • 즉 노드 하나를 어느 정도 크기로 가져갈지, 또한 포인터와 키 값을 어느정도 크기로 가져갈지에 따라 fanout이 고정되게 된다.
      • 루트가 아닌 nonleaf 노드는 $⎡n/2⎤$ ~ $n$개의 children을 갖는다.

        • 자료구조를 배울 때 많이 나오는 binary 트리는 최대 자식노드가 2개이기 때문에 트리의 깊이가 깊고 몸체가 얇다.
        • 메모리하고 하드디스크는 속도 차이가 1000배정도 나기 때문에 메모리에서는 일반적인 binary 트리와 같이 세로로 길어도 상관이 없지만 하드디스크는 속도가 느리니까 가능하면 노드의 크기를 크게 가져가고 깊이를 좁게 하고자 한다. 즉 가능하면 하나의 노드에 많이 집어넣으려고 한다.
        • 따라서 b+tree는 자식노드가 100개 이상으로 굉장히 많아 트리의 깊이는 얕고 몸체가 두꺼운 형태를 갖는다. 즉 절반 이상이 차도록 만든다.
      • 마찬가지로 leaf 노드 역시 $⎡(n–1)/2⎤$ ~ $n–1$ 개 사이의 값을 갖는다.

        • 트리들은 노드들간 포인터로 연결되는데, binary 트리는 memory용 자료구조로 memory는 접근시간이 빠르기 때문에 깊이가 깊더라도 빠르게 검색을 할 수 있다. 데이터베이스에서 자주 사용하는 b+트리는 보조기억장치 혹은 많은 사용자가 공유하기 때문에 자료 접근에 상대적으로 많은 시간이 걸린다. 그래서 트리의 깊이를 얕게 한 것이다.
      • 스페셜 케이스

        • 데이터 사이즈가 작을 떄는 루트가 위의 조건을 만족시키지 못할 수도 있다.
        • 만약 root가 leaf가 아니면 최소한 2개의 children을 갖는다.
        • 만약 root가 leaf이면(트리에 다른 노드가 없는 경우. 노드가 하나밖에 없는 경우) root는 $0$ ~ $(n–1)$개 사이의 values를 갖는다.
    • leaf node의 특징

      • leaf 노드 키 값 옆에 있는 포인터는 해당 키 값을 갖는 데이터가 하드디스크에 어디에 있는지를 가리키고 있다.
      • 특정 키 값이 여러개 있는 경우는 해당 키 값 아래에 붙게 되고(동명이인 같은 경우) 포인터는 가장 위를 가리킨다.
      • 키 값이 작은 것이 앞에 나온다. 즉 사전 순으로 정렬을 했을 때 키 값이 작은 것이 앞에 나온다.
        • $L_i$와 $L_j$가 있고 $i < j$라면 $L_i$의 serach key가 $L_j$의 search key보다 앞에 나온다.
      • 맨 마지막 포인터 $P_n$은 옆에 있는 노드를 가리킨다.
    • Nonleaf Node

      • leaf node와 전반적으로 똑같지만 차이점이 있다면
      • 포인터가 실제 데이터 레코드를 가리키고 있는 것이 아니라 child 노드를 가리키고 있다.
      • 그리고 $P_1$은 $K_1$보다 작은 키 값을 갖는 노드를 가리키고 있다.
      • $P_2$는 $K_1$보다 같거나 크면서 $K_2$보다 작은 노드를 가리키고 있다.
      • 즉 포인터는 키 값이 자기 앞에 있는것보다 같거나 크고 자기 뒤에 있는 것보다 작은 노드를 가리키게 된다.

Queries on B+ Trees

  • Search (탐색)

    • Mianus를 찾고 싶다.
    • Mianus의 키 값이 있는 leaf 노드로 가서 거기에 있는 포인터를 따라가면 그 데이터가 있다.
    • 일단 루트로 들어간다. 루트로 가서 search key값이 찾고자 하는 값보다 큰 것 중에서 가장 작은 값을 갖는 키(Perryrridge)를 찾는다.
    • 그런 것(value $K_i$)이 존재하면 해당 노드를 가리키는 포인터($P_i$)를 따라간다. 그렇지 않으면 제일 오른쪽($P_m$, 여기서 m은 node에 있는 포인터들의 수)으로 내려간다.
    • 내려와서 찾고자 하는 키 값보다 큰 키 값들 중에서 가장 작은 값을 찾는다. 그런데 Mianus보다 큰 키 값이 없다. 이렇게 없으면 제일 오른쪽으로 내려간다.
    • 이렇게 따라 와보니까 leaf 노드다. leaf 노드에 왔으면 내가 찾고자 하는 키 값을 찾는다. Mianus가 있으므로 왼쪽 포인터를 따라 내려간다.
    • 그러면 내가 찾고자 하는 Mianus가 거기에 들어있다.
    • 만약 leaf까지 왔는데 내가 찾고자 하는 키 값이 없으면 내가 찾고자 하는 것이 없는 것.
  • B+트리 쿼리의 특성
    • 쿼리를 처리할 때, 루트로부터 leaf 노드까지 인덱스 트리의 하나의 path를 탐색해 나간다. 즉 한 줄로만 내려간다는 것. 여러 줄로 내려가는 경우는 없다.
    • 만약 파일에 search key값이 K개가 있다면 검색 path의 길이는 $⎡log_{⎡n/2⎤}(K)⎤$보다 길지 않다.
      • 네이버나 구글을 생각하면 search key값의 갯수은 K는 엄청나게 크다.
      • n은 fanout. $n/2$는 최소 한도로 차 있는 데이터의 갯수. 많이 차 있어야지 트리가 짧아지니까 빨리 찾을 수 있다.
    • 예를 들어 search key values의 수가 100만이고 fanout인 n이 100일때, 검색 횟수는 최대 $log_{50}(1,000,000) = 4$밖에 안된다. 굉장히 빨리 찾을 수 있다.
  • Update (삽입, 삭제, 갱신)
    • 어려운점
      • 갱신은 탐색하고 똑같지만 삽입 삭제는 node overflow가 생길 때 node split을 해야하므로 어렵다.
      • node underflow도 있다. 데이터 삭제에서 발생. 절반 이하로 떨어지게 되면 underflow가 생긴 노드를 합쳐야한다.
      • 밸런스를 항상 유지해야 하기 때문에 또 어려움이 발생
    • Insertion (삽입)
      • 일단 내가 들어갈 자리(leaf node)를 찾아야 한다.

      • 해당 leaf node의 search key값이 있으면

        • 그 뒤에 붙이면 된다.
      • 하지만 없으면
        • leaf node에 공간이 있으면 (key-value, ponter)의 쌍을 leaf node에 넣고 해당 포인터 아래에 데이터를 넣는다.
        • leaf node에 공간이 없으면 overflow가 발생. 기존의 데이터와 새로운 키값을 가지고 노드를 분할해야 한다. 그 다음에 새로운 데이터를 넣는다.
        • 분할의 경우는 처음 $⎡n/2⎤$개를 원래의 노드에, 그리고 나머지를 새로운 노드에 넣고 parent node를 생성한다!
      • 삽입 Example 1

        • 5, 8, 2, 10, 7, 1, 20, 21, 15, 4, 16 의 데이터를 삽입한다고 가정해보자

        • 이때 한 노드에는 3개의 키 값이 들어갈 수 있는 n = 4라고 가정

          1. 5, 8, 2를 삽입한다. 당연히 정렬되어 들어간다.

          2. 10을 집어넣으면 overflow가 발생한다. $⎡3/2⎤ = 2$개의 노드를 원래의 노드에, 나머지는 새로운 노드에 삽입하고 8을 parent node로 올린다.

          3. 7을 집어넣는다.

          4. 1을 집어넣으면 overflow가 발생한다. 마찬가지로 2개의 노드를 원래의 노드에 남기고, 나머지는 새로운 노드에 삽입하고 5를 parent node로 올린다.

          5. 20을 집어넣는다.

          6. 21을 집어넣으면 overflow가 발생한다. 마찬가지로 2개의 노드를 원래의 노드에 남기고, 나머지는 새로운 노드에 삽입한다. 20은 parent node로 올린다.

          7. 15를 집어넣는다.

          8. 4를 집어넣는다.

          9. 16을 집어넣으면 overflow가 발생한다. 부모 노드에도 overflow가 발생하므로 노드 분할을 진행해준다. 최종적으로 위와 같은 트리 형태를 띄게된다.

      • 삽입 Example 2

        • 위와 같은 트리는 색인구조. 마지막 leaf 노드의 포인터가 가리키는 버킷은 실제 데이터 파일들.
    • Deletion

      • Deletion에서는 underflow가 발생.

      • 데이터를 삭제할 시 데이터의 양이 절반 이하로 떨어지게 되면 인접한 노드하고 합치는 과정이 필요

      • 삭제 Example 1

      • 삭제 Example 2

      • 삭제 Example 3