Search

Index And Hashing

Index

Dense

모든 레코드마다 index가 하나씩 다 달려있는 상태
이게 왜 좋냐?
index block의 숫자는 data block보다 더 작다 → key랑 포인터만 있으니까
Key는 정렬되어있고 binary search를 할 수 있다
index는 메모리에 들어갈만큼 작다.

Sparse

block마다 key-pointer pair가 달려있는 상태
dense보다 더 적은 공간을 차지하지만, record 찾는데는 시간을 더 쓴다.
전체 n개의 record가 있을 때, 한 개의 block은 3개 레코드를 가지거나 10개의 key-pair 포인터를 가진다. 데이터 파일 + dense index: ceiling(n/10){인덱스} + ceiling(n/3){데이터 파일} 데이터 파일 + sparse index: celing(n/30){인덱스} + celing(n/3){데이터 파일}
Markdown
복사

Multiple Level

index 파일이 여전히 크면 추가적인 레벨이 dense 이거나 sparse여야하나?
sparse여야한다. dense여봐야 소용이 없다.
블럭에 대한 reduction이 없기 때문이다.

Secondary Index

primary index와 다르게, 레코드의 위치를 결정하진 않는다.
random한 order로 되어있기 때문에
sparse index에서 정렬이 되어있다하더라도, 어차피 해당 block 내에서 정렬이 안 되어있기 때문에 찾을려고 하는 값이 어디있는지 알 수 없다. (sparse는 블럭 별로 하기 때문에 정렬이 안 되어있으면 그 블럭을 따라가더라도 값이 그 안에 무조건 있을지 확신할 수 없음) 따라서 무조건 dense가 되어야한다.
redundant key를 막으려면, multiple level을 적용하면 된다.
언제 좋느냐
clustered file 구조에 적합함
SELECT title, year FROM Movie, Studio WHERE presC# = zzz AND Movie.studioName = Studio.name;
Markdown
복사
이런 쿼리가 빈번하게 일어난다면, clustered file 구조가 유리하다.
위의 그림과 같은 구조로 clustered 될텐데, 이런 경우 studio에 대해서 presC#에 대해 secondary index를 만드는 것이 좋을 것.
물론 그에 딸린 movies에 대해서도 index가 걸려있어야, 쉽게 찾을 수 있을 것
key가 포인터보다 크고 key가 평균적으로 두 번 등장할 때
이렇다면 indirection을 이용해서 unique하게 만들 수 있기 때문
대부분의 레코드를 보는게 아니라 bucket 포인터만 사용할 수도 있다.
select title from movie where studioName = ‘Ghibli’ and year = 2008;
이 경우 대해서 studioName과 year에 대해 secondary index가 있다면 겹치는 부분만 생각하면 된다.

Inverted Index

앞의 아이디어가 text 정보를 뒤질 때 사용됨.
‘cat’과 ‘dog’라는 정보를 가지고 있는 document를 찾고 있다해보자
cat, dog에 대해서 어떤 doc에 등장하는지를 다 가지고 있으면, 그 교집합도 쉽게 찾을 수 있을 것

B-Tree

paramters & 구조

각 block은 n개의 search key와 n+1개의 포인터가 있다.
Rule
leaf node의 key들은 data file로부터온 keys의 복사본이다. 이 키들은 leaf에 대해서 왼쪽에서 오른쪽으로 sorted가 되어있다.
root에서, 최소 2개의 포인터가 사용되어야한다. 모든 포인터들이 바로 아래 단계의 B-tree 블럭을 가리켜야한다.
leaf에서 마지막 포인트는 그 다음 leaf block을 가리켜야한다. leaf block에 있는 n개의 포인터들 사이에서, 최소 (n+1)/2의 버림만큼의 포인터는 사용이 되어야한다. 사용되지 않은 애들은 null이고 아무곳도 가리키면 안 된다.
중간 노드에 대해서는 n+1개의 포인터가 아래 레벨 블럭을 가리키는데 사용될 수 있다. (n+1)/2 올림만큼이 최소한으로 사용이 되어야한다.
K번째 pointer에 대해서는 K-1 key ≤ keys ≤ K key
pointers and keys
Node type
Min #pointers
Max #pointers
Min #keys
Max #keys
Interior
(n+1)/2 올림
n+1
(n+1)/2 올림 -1
n
Leaf
(n+1)/2 내림
n+1
(n+1)/2 내림
n
Root
2
n+1
1
n
Interior
n+1개의 pointer가 전부다 아래쪽을 가리키는데 쓰이니 n+1/2 올림으로 계산
min key의 경우, key값 자체가 pointer보다는 하나가 작으니 1을 빼준다.
Leaf
마지막 포인터가 항상 그 다음 노드를 가리키고 있으니 전체의 절반의 버림 처리를 해줘야함.
Min key 같은 경우에 key값과 포인터가 항상 특정 블럭을 가리키도록 같아야하므로 Min Pointers과 Min keys는 그 숫자가 같아야함.
Root
Root의 경우 최소한 두 개로 나뉘기는 해야하니 Min pointer값이 2이고 이에 따라 Min key값은 1이다.
다만 record가 1개인 경우에는 실제로 한 개가 있을 수도 있다.

Look up

값 찾기
그냥 binary search로 가면 된다.
범위로 찾기
a≤ x ≤ b인 경우에 a가 있든 없든 a보다 크거나 같은 첫 leaf로 간다음 b보다 크지 않다면 다음 노드로 이동함.
범위가 한 방향인 경우 a≤ x 인 경우에, a보다 크거나 같은 친구부터 끝까지 간다.

Insertion

적절한 leaf에 넣는다. room이 있으면 넣음
room이 없다면, leaf를 반으로 쪼갠다. 각각 half-full이거나 half-full을 막 넘는다.
node를 쪼갠다는 건, 바로 위 레벨에 새롭게 생긴 친구를 가리킬 key-pointer pair가 필요하다는 말. 따라서 그 위의 레벨에 집어넣는 것을 똑같이 반복함.
예외적으로 root에 넣는데, room이 없다면, root를 쪼개고 새로운 higher level을 넣는다.
split node를 하고 부모에 집어넣을 대, key 관리를 잘 해야한다.
N이 leaf node이고, capacity가 n일 때
n+1째 key를 넣으려고 함.
new Node M
첫번째 올림(n+1/2)가 N에 들어가고 나머지는 M으로 간다.
N이 interior node이고, capacity가 n일 때
N에 대해서 n key가 있고 n+1 pointer가 있을 때, 위에서 쪼개져서 막 하나가 올라왔다. n+2 포인터가 됫음
M을 새로 만들고.
첫 올림((n+2)/2)만큼을 N에 남겨놓고, 남은 버림((n+2)/2) 만큼을 M으로 옮긴다.
올림(n/2) key를 N에 남겨놓고, 버림(n/2) key를 M으로 옮기면 딱 하나가 남음.
이 하나는 M의 자식들 중 첫번째에 갈 수 있도록 하는 key이다. 이 key가 N이나 M에 나타나지 않더라도 M에 있어야한다. K는 N의 부모와 M에 추가된다.

Deletion

삭제했는데, 해당 노드의 유지가 더 이상 어려울 때 두 가지 방법이 있음
1번
N에 인접한 sibiling 중 하나가 mnimum number보다 많은 키를 가지고 있으면, key-pointer 페어 하나만 들고온다.
아마 N의 부모에 있는 key들 또한 조정되어야할 것.
N,M에 대해서 M이 right of N일 때, 옮겨지는 key는 아마 가장 작은 key일 것이다. 새로운 M을 반영하기 위해 parent of N,M의 키가 바뀌어야할 것.
2번
인접한 sibiling에서 제공해줄 수 없을 때이다.
이 때는 sibiling 끼리 합치면 된다.
부모의 key를 조정하고 parent에 있는 key-pointer를 삭제하면 된다. 만약 충분하면 상관없는데, 아니라면 parent에 대해 똑같이 적용하면 됨

Duplicate Keys

정의를 약간 바꿔야한다.
new key
Ki에 대해서 i+1번 째의 subtree에는 최소 하나 존재하는데, 그 왼쪽에 있는 subtree에 대해서는 하나도 존재하지 않는 경우가 해당됨
Ki가 null로 되는 경우가 있다.
그에 해당하는 포인터들은 여전히 필요하다. 왜냐하면 오직 하나의 key value를 가지고 있는 트리에 중요한 portion을 차지하기 때문이다.(???)
23보다 작은걸 찾으려고 할 때 root의 두번째 child로 일단 올거다. 그리고 그것의 두 번째 child에서 시작하고 싶지는 않다. 왜냐하면 첫번째 key가 Null이기 때문에 두 번째 child에는 새로운게 없다는 뜻이기 때문.
만약 위의 예시에서 24부터 26까지를 찾는다고 한다면 왼쪽에서 세번째노드, 그니까 첫번째 child로 바로 갈거다. 위와 같은 이유로. 그렇게 해서 찾고자 하는 값이 없다면 사실 오른쪽으로 가볼 필요가 없다. 왜냐면 첫 key가 null이라서 그 다음꺼에는 새로운게 없을 거라는 확신이 있기 때문이다.
만약에 우리가 찾는게 있었다면 null이 아니라 새로운 값이 Key로 박혀있었어야하고, 만약에 다섯번째 leaf에 있었다면 37 대신 그 값이 박혀있었을 거다.
렉처노트 예시
만약에 24를 찾는다고 하면, 일단 두 번째 interior 노드로 감
첫 번째가 null이고, 두 번째가 37임.
따라서 17부터 37 사이에는 new key가 없음
따라서 첫 번째 리프에 갔을 때, 24가 없다면 두번째 리프에서부터 시작하는 새로운 키가 없기 때문에 null이 들어가있는 것이므로 두 번째 리프 그 이후를 볼 필요가 없음. (모든 값들이 첫 번째 리프에서 들어가있다는 뜻). 따라서 안 봐도 없다는 걸 알 수 있음

Efficiency

B-tree reorganization은 negligible 하다.
number of I/O = tree level + one or two for record manipulation
root block은 영원히 메모리에 남겨놔야 save I/O 할 수 있다.

Hash Table

1.
block이 consecutive on disk
2.
main memory array of pointers를 사용 ⇒ consecutively하게 저장할 필요 없음

Static & efficiency

버킷 수 고정
버킷에 레코드가 많으면 overflow qkftod
Insertion
Deletion
Efficiency
enough bucket이어서 한 블럭에 들어가길 원함. 체이닝 없이
그러면 Lookup은 1 disk I/O 이다.
Insert/Delete ⇒ 2 I/O
파일이 grow할 때, long chain이 생김

Extensible

첫 i bit를 사용한다.
bucket에는 블럭에 대한 포인터가 있음
Insertion & Deletion
Deletion에 대해서는 Insertion의 거꾸로인데, merging하는 건 optional 임
Handling duplicated key
splitting으로는 해결을 못하는 거고, overflow block을 사용해야한다.
요약
bucket에 메모리 안에 들어간다면 1 disk I/O
약간의 공간적 소비를 하고 full reorganization은 피한다.
double bucket array는 비싸다.
splitting이 블럭당 레코드 수가 작으면 자주 일어나고
어떤 포인트에서 bucket array가 memory에 딱 맞지 않을 수 있다.

Linear Hashing

last i bits를 사용함
bucket을 추가했는데, 재배치할 때 적당한게 없으면 그대로 있는다.
찾을 때 자릿수에 대해서 다 맞는게 없으면 한 자리씩 줄여가면서 찾는다.
little wasted space를 하면서 grow & avoiding full reorganizations할 수 있다.
extensible array에 비하면 array of bucket이 없다.
long chain of overflow가 생길 순 있음
Indexing이 range 검색에는 좋고, Hashing이 Equality based 검색에는 좋다.