Index Structures


What is INDEX?

Why do we need it and What is it?

  • 다양하고 방대한 블록들에 흩어져 있는 레코드를 표현하기란, 여간 간단한 일이 아니다.
  • 모든 블록들은, 레코드의 시작과 끝, 그 레코드에 속하는 릴레이션이 무엇인지 알려주는
    '레코드 헤더' 에 대해 많은 정보들을 알고 있어야 한다.

'인덱스(Index)' 라는 자료 구조를 통해 원하고자 하는 레코드의 정보를 '빠르게'찾을 수 있다. 
또한 인덱스 자체도 물리적 구조이므로, 인덱스 데이터의 삽입/삭제도 꽤 복잡하다.
이런 인덱스 값을 기반으로 하는 필드를 'Search key(검색 키 혹은 탐색 키)' 라 한다. 
많은 다양한 인덱스의 구조가 있지만, 지금부터 살펴볼 내용은

  1. Simple indexes : 정렬되어있는 파일에 대한 간단한 인덱스
  2. Secondary indexes : 정렬되어있지 않은 파일에 대한 인덱스
  3. B-trees : 가장 널리 쓰이는, 트리 구조 인덱스
  4. Hash tables : 비트리와는 다르지만 또한 유용한 인덱스

https://www.lucidchart.com/publicSegments/view/6e3a6bc1-ac27-4ff2-b0d1-d6d7304be334/image.png 




Keys and More Keys

Key라는 건 더 많은 의미가 있다.
primary key, sort key 등...
Search key는 값이 주어지며, 
인덱스로 tuple의 matching-value를 찾기 위한 Column(s)를 말한다.

primary, sort, search등 Key의 의미가 명확하도록
이러한 표현들을 덧붙인다!


Indexes on Sequential Files

우선, 간단한 상황의 인덱스 구조에 대한 알아보자.
정렬된 파일인 Data file에게, Key-Pointer 쌍을 가지고 있는 Index file이 주어진다.
인덱스 파일 안의 Search key K는, 이 K를 보유한 Data file 레코드를
참조하고있는 포인터와 연관이 있다. 이러한 인덱스들은 기본적으로 두 가지로 구분할 수 있는데,

  • Dense Index : Data file의 모든 레코드 항목이 존재하는 인덱스 구조.
  • Sparse Index : 몇몇 데이터 레코드들만 표현되는 인덱스 구조. 보통 데이터 파일의 블록당 하나의 항목.



Sequential Files

  • Sequential File : 인덱스의 Column(s)에 의해 정렬되는 파일.
    이러한 구조는 Search key가 릴레이션의 PK일 때 특히 유용하다(useful).


https://www.lucidchart.com/publicSegments/view/f6e2fc45-d24a-46f7-bdc3-718efa2684fe/image.png 



시퀀셜 파일에서는, 튜플들이 PK에 의해 정렬되어 있고,
그림과 같이 일반적으로 key를 10 and 20, 즉 10의 배수 상태로 구별한다



Dense Indexes

Dense Index

  • Key와 레코드 자체에 대한 포인터만 보유하는 블록의 나열(인덱스).
  • 'Dense(:밀집한)'라 불리는 이유는, 데이터파일의 모든 키가 인덱스에서 표현되기 때문이다.
  • 덴스 인덱스는 Key들을 그 파일 자체 순서와 동일하게 유지한다.
  • 키와 포인터만 보유한 인덱스는, 완전한 레코드보다 훨씬 적은 공간을 차지하기 때문에
    파일 자체보다 훨씬 적은 공간을 소모할 것이다.
  • 즉, 인덱스는 데이터 파일을 위한다기보단, 메모리를 위해 설계된 것이다.
  • 또한 인덱스를 사용하면, 1회의 Disk I/O 만으로도 주어진 Search key를 이용하여 어떤 레코드던 찾을 수 있다.


https://www.lucidchart.com/publicSegments/view/7203baa6-7dce-4f04-98f1-f66ad6cb09d3/image.png 



  • Dense index는 인덱스 블록을 주어진 Search key 값 K로 검색하며, 찾을 경우 그 포인터로 연관된 레코드를 따라간다.
  • 여.기.서
    K를 찾을 때까지 인덱스의 모든 블록을 탐색해야한다. (평균적으로 절반만큼)
  • 성능이 좋아보이지 않지만, 인덱스 기반의 서치는 보기보다 효과가 대단하다.


Why?

  1. 인덱스 블록의 수는 대개 데이터 블록의 수보다 비교적 적다.
  2. 키값들이 정렬되어 있기 때문에 이진탐색을 할 수 있다. 인덱스에 n개의 블록이 있다면, log2(n)만큼이 소요
  3. 인덱스는 메인 메모리 버퍼에 영구 유지될 수 있을 만큼 작다
    그렇다면 메인 메모리 액세스만 하면 되기 때문에 expensive한 disk I/O 수행이 필요 없다.



Sparse Indexes

Sparse Index

  • Dense index는 모든 키를 가지고 있기에는 비교적 크다.
  • 이런 단점을 보완하여 설계한 것이 바로 이 Sparse Index이다.
  • 데이터 블록마다 오직 한 개의 Key-pointer 쌍을 보유한다.
  • 훨씬 적은 공간을 사용하지만, 주어진 레코드의 key를 찾는데 더 많은 시간이 걸린다.
  • 인덱스 블록만 보고, 찾고자하는 K가 존재하는 지 아닌지를 알 수 있는 Dense index와는 달리
    같은 쿼리문으로, Sparse index는 해당 블록에 K가 '있을지도 모르니' disk I/O를 요구한다.


https://www.lucidchart.com/publicSegments/view/b86349a6-1468-49be-bc4f-893c5ec5fe19/image.png 



  • Sparse index는, 주어진 search key 값 K와 작거나 같은 값 중 가장 큰 값을 찾는다.
  • 역시 인덱스 키 값들이 정렬되어 있기 때문에 이진 탐색이 가능하며, 찾을 경우 그 포인터로 연관된 블록을 따라간다. 
  • 그 뒤로, key값 K를 찾기 위해 블록을 탐색한다. 
    물론 블록은 각 레코드의 컨텐츠가 식별될 수 있을만큼 충분한 정보를 가지고 있어야 한다.(저번에 살펴본)



Multiple Levels of Index

Multiple Level Index)

인덱스 자체로는, 많은 블록을 커버할 수 있다는 장점이 있지만, 
이진탐색으로 인덱스 항목을 찾더라도, 원하는 레코드를 찾고자 
여전히 수차례의 Disk I/O를 수행해야 할 수도 있다.
이럴 땐, 인덱스를 위한 인덱스를 둠으로 써, 인덱스를 좀 더 효율적으로 사용할 수 있다. 
https://www.lucidchart.com/publicSegments/view/395be250-b56a-4507-896c-9b217e909e79/image.png 



  • 그림은, 원래의 sparse index에 레벨을 하나 더 추가한 것이다.
    • 원래의 인덱스를 First level index(혹은 inner index),
    • 추가한 인덱스를 Second level index(혹은 outer index)라 한다.
    • 같은 아이디어로, third, fourth 등 계속해서 추가할 수 있다.





      Warning

      조그마한 주의사항으로, 
      최초의 인덱스가 아니라면(최초의 인덱스는 dense/sparse 둘다 가능) 
      반드시 sparse index여야 한다. (당연하게도..아니면 의미가 없어지므로) 

      하.지.만 
      이 원리는 한계가 존재하고, 많은 레벨의 인덱스들을 B-tree 구조로 빌드하는 것이 더욱 좋다. 



      Indexes With Duplicate Search Keys

      레코드의 Search key가 중복이 없다면, Dense index는 의미가 없을 수도 있다.

      -> (모든 레코드의 값을 인덱스에 반영하므로)

      하지만 대개의 경우, Search key의 중복이 여럿 발생하여 index를 배치한다.
      이러한 중복을 해결하기 위해, 중복되는 Key값에 대해서는 한개의 인덱스 항목과 포인터만을 두자.
      즉, 

      • Dense Index의 경우, 중복 Key들에 대해 첫 번째 Key 값에 대해서만 인덱스 항목과 포인터를 두게 설계.
      • Sparse Index의 경우, 블록당 한 개의 인덱스 항목과 포인터를 두며, 그 포인터는 항상
        블록의 첫 번째 Key 값만을 가리키게 설계.


      https://www.lucidchart.com/publicSegments/view/b64de209-c1df-4521-96a3-b6caa1ca3434/image.png 




      https://www.lucidchart.com/publicSegments/view/af4e3b03-d3d1-4d58-8e1c-7ef14f1cdae3/image.png 




      Managing Indexes During Data Modifications

      지금까지는 고정된, 정렬된 데이터 파일에 대해서만 인덱스를 살펴보았다. 
      하지만, 데이터는 시간이 지남에 따라 발전하기 때문에, 레코드는 삽입/삭제/갱신될 수 있다. 
      앞으로는 Sequential File(순차적 파일)같은 구조도, 한 블록에 들어 맞는 것이 존재하지 않게(더욱 거대하게) 변할 것이다. 
      저번에 알아봤듯, 데이터 파일을 재구성 하는 방법처럼, 다양한 기법을 사용해야 한다. 

      1. Overflow block 생성/삭제 : 오버플로 블록은 스파스 인덱스에 존재하지 않고, 차라리 기본 블록의 확장으로 간주되어야 한다.
      2. 오버플록 대신, 순차적인 정렬 하에 새로운 블록을 삽입할 수 있다.
        이 경우, 새로운 블록에 대한 항목을 스파스 인덱스에 추가해야 한다.
        즉, 데이터 파일의 삽입/삭제 시 발생한 문제들과 같은 현상이 발생할 가능성이 있다는 걸 염두하자. 
        (인덱스 역시 물리적 공간에 저장되어야 하기 때문)
      3. 튜플을 블록에 삽입할 공간이 없으면, 인접 블록(Adjacent Block)에 끼워 넣을 수 있다.
        반대로 또 인접 블록이 너무 텅텅 비게 되면 Combined(병합)될 수도 있다.

      하지만, 데이터 파일의 변경은 해당 인덱스의 빈번한 변경을 야기한다.
      정확한 방법은, 인덱스가 덴스/스파스 인지, 그리고 위에 열거한 세 가지 방법 중 어떤걸 사용하는지에 달려있다.
      기억해야 할 하나의 기본 원리는

      • 인덱스 파일 자체도 파일의 나열(Sequential file)이다. 
        ( 키-포인터 쌍은 search key값으로 정렬된 레코드로 취급 ) 
        그러므로, 데이터 파일을 유지보수 하는 데 사용한 것과 동일한 전략을 사용할 수 있다. 


       Actions Dense Index  Sparse Index 
       Create empty overflow block  none none
       Delete empty overflow block  none none
       Create empty sequential block  none insert
       Delete empty sequential block  none delete
       Insert record  insert update(?)
       Delete record  delete update(?)
       Slide record  update update(?)



      위의 표에 나와있듯, 우리가 숙지해야 할 것들은,

      • 빈 오버플로 블록을 생성/삭제하는 것은 어떤 인덱스든 효과가 없다.
        덴스 인덱스의 경우, 레코드를 참조하므로 무의미하고
        스파스 인덱스의 경우, 참조하는 것은 오버플로 블록이 아닌 주 블록만 참조하기 때문이다.
      • 빈 정렬된 블록을 삽입/삭제하는 것은 
        덴스 인덱스에겐 효과가 없지만, (마찬가지로 블록을 보는게 아니라 레코드를 보기 때문)
        스파스 인덱스의 경우 각 블록의 삽입/삭제에 따른 인덱스 삽입/삭제가 반드시 필요하다.
      • 레코드의 삽입/삭제에 대해서는, 
        덴스 인덱스는 무조건 key-pointer 쌍이 같이 변경되지만
        스파스 인덱스는 그 레코드가 블록의 첫부분이냐 아니냐에 따라 update/none이 결정되기 때문에 물음표를 첨가했다.
      • 레코드 끼워넣기
        덴스 인덱스는 같이 가고,
        스파스 인덱스 역시 그 끼워넣는 부분이 블록의 어디냐에 따라 달라진다.



      https://www.lucidchart.com/publicSegments/view/d3a29a46-7124-4064-803f-8ee2246d53b5/image.png 



      https://www.lucidchart.com/publicSegments/view/97edb1b2-95f9-4f36-80a4-5fd35d26e457/image.png 




      Secondary Indexes

      여태껏 살펴본 '인덱스'들은, 'Primary Indexes'라고 할 수 있다. 
      즉, 인덱스가 레코드와 직접적으로 연결되어 위치를 결정하기 때문인데,
      다양한 쿼리문을 좀더 용이하게 하기 위해 대부분의 경우 한 릴레이션에 대해 여러 개의 인덱스를 두고자 한다.

      예시)

      students ( name, dept_name, grade, ... )

      SELECT  name
      FROM  students
      WHERE  dept_name = 'Comp. Sci.';
      
       name이 PK인 students라는 릴레이션에서(인덱스는 name에 관련한 쿼리를 돕기 위해 name으로 생성하겠지만,
       dept_name이 컴공인 학생을 검색하고 싶을 때
      
       -> dept_name을 search key로 가지는 인덱스가 있다면 더 효율적인 검색을 할 수 있다.(보조 인덱스의 필요성)
      

      이렇듯 Secondary Index(보조 인덱스)는, 다른 인덱스를 돕는, 말그대로 보조 인덱스이다.
      주 인덱스와의 가장 큰 차이로, 이 보조 인덱스는 데이터파일 내 레코드의 위치를 결정짓지는 않는다는 것이다.
      위치를 결정해주는 건 주 인덱스고, 보조 인덱스는 그저 레코드의 위치가 어딘지 알려주는 역할을 할 뿐이다. 

      이 두 인덱스의 차이점은, 

      • 보조 인덱스로 스파스 인덱스를 사용하는 건 말도 안된다
        보조 인덱스는 위치에 영항을 끼치지 않기 때문에, 인덱스에서 명시적으로 언급을 해야지만 레코드의 위치를 알 수 있다. 
        -> 즉, 보조 인덱스는 항상 덴스 인덱스이다.



      Design of Secondary Indexes

      Design

      • 보조 인덱스는 덴스 인덱스이며 중복이 보통 발생한다. 
      • Key-pointer 쌍의 key는 search key 이며, 유니크할 필요는 없다.
      • 주어진 Key 값으로 항목을 찾기 위해, 인덱스 파일의 쌍은 Key 값에 의해 정렬된다.

      만약 상위 레벨의 인덱스를 설계하고싶으면 그 인덱스는 스파스이다!

      보조 인덱스와 멀티레벨 인덱스를 혼동하지 말자.


      https://www.lucidchart.com/publicSegments/view/c7f3dbe6-efb2-44b6-a899-7b4ee37d9e6b/image.png 



      • 데이터는 정렬되어 있지 않아도 인덱스는 정렬되어 있다. 
      • 20이 무려 세개나 있는데, 이렇게 보조 인덱스를 이용할 땐
        주 인덱스만 이용할 때보다 Disk I/O횟수가 오히려 더 많을 때도 있다.
      • 하지만, 데이터 블록의 튜플들은 아마 다른 column(s)에 의해 정렬된 상태일 테니 
        우리가 순서를 조정할 수 없어 이 문제는 해결할 수 없다!



      Indirection in Secondary Indexes

      • 위의 그림처럼 보조 인덱스를 설계한다면,
        상당히 많은 공간의 낭비가 생길 수밖에 없다.
      • 즉, 데이터 파일의 Search key 값이 n회 나타나면 인덱스파일에도 n회 기록될 것이다.
      • 반대로, 특정 값을 가진 데이터 레코드에 대한 모든 포인터에 대해서, 키 값을 한 번만 쓴다면 더 나을 것이다.


      https://www.lucidchart.com/publicSegments/view/54e61814-7361-4f74-bf42-0c6dacb3697c/image.png 




      B-Trees

      What is B-Tree?

      1~2 level의 인덱스도 쿼리의 속도를 많이 향상시켜 주지만,
      실제 상용 시스템에서 더 널리 쓰이는 구조가 있다.
      바로 'B-tree' 라 하는 데이터 구조이며,
      이 중에서도 특정 변형 형태인 'B+tree'를 가장 많이 사용한다. 
      이제 B-tree에 대해 설명할것이지만, Detail은 모두 B+tree로의 변형을 위한 것임을 염두하자. 

      • B-tree는, 인덱스를 생성할 파일의 크기에 적절한 만큼의 인덱스 레벨을 자동으로 유지한다.
      • 블록의 공간도 관리하여, 모든 블록은 꽉 차있거나 최소 절반 이상 차있게 된다. 오버플로블록은 필요없다.

      The Structure of B-trees

      Structure

      이름에서 알 수 있듯 B-tree 구조는, 블록을 트리로 구성한 것이다. 
      B는 'balanced'의 약자이며, 이 밸런스는 루트로부터 리프까지 모든 경로는 같은 길이라는 것을 의미한다. 
      B-tree에는 보통 세 종류의 레이어 ( Root, leaves, intermediate layer )가 있다. ( 더 추가할 수도 있다 )

      • Root : 루트 노드
      • Leaf : 리프 노드 (트리의 외부 노드(External node))
      • Intermediate layer : 루트와 리프가 아닌 중간 단계에 있는 노드

      각 인덱스와 관련된 파라미터 n이 존재하며, 이 n은 B-tree의 모든 블록의 레이아웃을 결정하게 된다.

      • 각 블록은 n개의 Search key 값과 n+1개의 포인터를 위한 공간이 존재한다.
      • 리프 노드의 Key 값은 데이터 파일의 Key값이다. 이 Key값은 왼쪽에서 오른쪽으로 정렬되어있다.
      • 루트에는 적어도 두 개의 포인터가 사용된다. 또한 모든 포인터들은 하위(자식) 노드를 가리킨다.
      • 리프의 마지막 포인터는 그 다음 크기의 Key를 보유한, 바로 오른쪽의 노드를 가리킨다.
      • 또한 리프 블록 내의 다른 n 개의 포인터들 중, 적어도 (1+n)/2(나머지 버림)개의 포인터들은 데이터 레코드를 가리킨다. 
        (안쓰이는 포인터는 널을 의미하며 아무데도 가리키지 않음)
      • 또한 i번째 포인터는 데이터의 i번째 key를 보유한 레코드를 가리킨다.
      • 중간 노드들의 모든 n+1개의 포인터들은 그 다음 하위의 B-tree 블록을 가리킨다. 
        적어도 (1+n)/2(올림)개의 포인터가 사용된다.

      https://www.lucidchart.com/publicSegments/view/08895b1f-2111-4790-a99e-e5d9b101f7a3/image.png 




      Lookup in B-Trees

      B-tree 인덱스를 가지고, Search-key K를 보유한 레코드를 찾는 작업을, 'Lookup' 이라 칭한다. 
      K를, 루트부터 리프까지 Recursively(재귀적)으로 찾는다.

      • BASIS : leaf에 도달하면, 거기서 key를 보자. i번째 key K라면, i번째 포인터가 레코드를 가리키고 있을 것이다.
      • INDUCTION : 중간 내부 노드 (K1, K2, ... Kn)라면 원하는 key값 K를 찾아 child로 내려간다. 이 내려가는 절차를 재귀로 적용한다.



      Range Queries

      B-tree는 search key의 단일 값이 요구되는 쿼리문 뿐만 아니라,
      값의 범위를 요구하는 쿼리문에서도 유용하다.
      'Range queries' 는 WHERE 절에 '=' 나 '<>' 를 제외한 비교 연산을 이용하여 search key 값(들)을 비교하는 형태를 일컫는다.

      예 :

      SELECT  *
      FROM  R
      WHERE  R.k > 40;
      
      or
      
      SELECT  *
      FROM  R
      WHERE  R.k >= 10 AND R.k <= 25;
      
      
      • [a, b]범위 내 모든 key를 B-tree의 리프에서 찾으려면, 먼저 a를 찾는다.
      • a가 있든 없든 a가 있어야 할 위치를 알게 될 것이고, a가 있다면 a부터a가 없다면 a보다 큰 제일 작은 값부터 범위 내에서 찾기 시작한다.
      • 즉, 계속해서 다음 리프를 찾아가다가 마침내 b보다 큰 값을 보유한 노드를 찾게 되면, 그 지점에서 멈춘다.
      • a가 마이너스 무한대여도, b가 무한대여도 (바운더리가 없다는 뜻일 뿐이므로) 충분히 가능하다.




      Insertion Into B-Trees

      Insertion

      앞전에서, simpler multilevel index는 한계가 존재하여 B-tree로 구성하는게 좋다고 했는데
      재귀에 따른 삽입 과정을 보며 그 이유를 살펴보자.

      • 새로운 Key가 위치할 적절한 리프 노드를 찾고(lookup), 그곳에 공간이 있으면 넣는다.
      • 오버플로가 발생하면, Split(리프 노드를 두 개로 나누고)
        이 두 노드에 원래 보유했던 Key를 나눈다((1+n)/2 올림 까지)
        그러면 각 두 노드는 절반이 차거나 아니면 절반 조금 넘거나 할 것이다.
      • 그리고선, 부모에 중간값을 올린다. 부모에 중간값이 들어가는 것 자체도 삽입 연산으로 취급한다.
      • 예외적으로, 루트에 삽입하려고 했는데 공간이 없어서 분할한 경우,
        이 두 노드를 자식으로 갖는 새로운 루트 노드를 상위 레벨에 생성한다.
        (루트 노드는 slot의 개수와 상관 없이 한 개의 key를 갖고 두 개의 자식을 갖는다.)

      노드를 분할하고 부모로 삽입할 때마다 키 관리는 무척 조심스럽게 해야한다.

      먼저, n개의 슬롯을 가진 리프 N을 가정하자.(슬롯은 다 꽉찬상태)
      그리고 이 리프에 n+1번째 key를 삽입한다고 가정해보자.
      그럼, N을 나눠 새로운 노드 M을 N 바로 오른쪽에 만든다.
      첫번째 부터 (1+n)/2까지의 key-pointer쌍은 N에 그대로 두고,
      나머지 쌍들은 M에 옮긴다. (여기서, 적어도 (1+n)/2개의 쌍을 가져야 한다는 걸 주의하자)



      < 파일첨부 >




      Deletion From B-Trees

      Deletion

      특정 Key값 K를 삭제하려면, 일단 레코드 위치를 찾고, 리프노드의 key-pointer 쌍에 대한 정보를 알아야 한다..
      찾은 후 삭제를 했는데, 이때

      1. 삭제가 발생한 노드가 최소한의 key-pointer쌍 개수 이상을 보유하고 있다면
        그대로 삭제 연산은 끝난다.

      하지만, 삭제 전에 최소의 개수를 가지고 있었을 수도 있어서,
      삭제 후 키의 개수 제약을 깰 가능성은 다분하다.

      1. 만약 부족하다면, 다음의 두 가지 방법 중 하나를 사용하여 B-tree 상태를 유지할 수 있다.
        • N의 인접한 형제 노드 중 하나가 최소의 key-pointer 쌍 개수보다 많이 보유하고 있을 경우,
          한 쌍을 N에게 전해주고, key의 순서를 그대로 유지한다. 
          이 때엔, N의 부모 노드가 새로운 상황을 반영해야 할 수도 있다.
          즉, 예를 들어 N의 오른쪽 형제 노드 M이, key중 가장 작은 값을 N에게 넘겨주면
          N,M의 부모 노드에게 M의 smallest key 값을 올려준다.
        • N의 인접한 형제 노드 중 아무도 최소 개수보다 많이 보유한 노드가 없는 경우도 있다.
          이 때엔 근접한 형제노드 두 개를 하나로 Merge(합친 뒤), 부모와 잘 연결해 주기만 하면 된다. 
          (합쳐도 한 노드가 가질 수 있는 최대 key-pointer 쌍의 개수보다 작거나 같으므로)
          부모가 삭제되어 또 최소 갯수를 만족하지 못한다면 이 알고리즘을 재귀로 돌려 부모 노드에서 시행한다.


      <파일첨부>




      Efficiency of B-Trees

      B-tree는 레코드의 위치찾기(lookup)/삽입/삭제 면에 있어, 각 파일에 대한 disk I/O 횟수를 무척 줄여준다.
      우선, 블록당 소유할 수 있는 key의 개수가 충분히 크면 Splitting / Merging같은 이벤트는 매우 드물게 된다.
      일어난다 해도, 거의 대부분은 두개의 리프 노드, 그들의 부모 노드 정도만 영향을 받는다.
      그러므로, I/O cost를 거의 무시할 수 있다고 봐도 과언이 아니다.

      하지만, search key로 레코드에 대해 '검색'하는 것자체가 루트부터 리프 노드까지 포인터를 찾는것이다. 
      즉, B-tree의 level 수만큼 disk I/O가 일어나고, 여기에 (lookup을 위한 1회), (insert/delete를 위한 2회) 
      정도의 횟수가 더 발생한다. 




      Hash Tables

      Structure of Hash Table

      해시 테이블 구조는,
      search key를 인자로 받는 'Hash Function' 이 존재하고, 이 해시함수는 받은 search key 값을
      'Bucket' (디스크 블록 같은 개념) 에 대응시킨다. (매핑한다)
      즉, 레코드가 search key K를 보유한다면, 버켓넘버 h(K)번째 리스트 버켓에 링크 해놓음을써 저장한다. (h는 hash function)


      https://www.lucidchart.com/publicSegments/view/2d0e154b-e36a-4729-964d-2e1bb9110deb/image.png 




      Choice of Hash Function

      'Hash function'은 key를 말그대로 'hash'할 수 있어야 한다.
      또한 결과값이 정수형으로 나와야 한다는 게, 꼭 랜덤함수처럼 보인다.
      이러한 해쉬함수는 보통 소수 bucket 수 B로 key 값 K를 나누는 형태를 띈다.
      만약 search key가 문자라면, 문자들을 다 숫자로 치환해 더한 값을 B로 나누는 식으로 시행한다.




      Insertion Into a Hash Table

      Insertion

      search key K를 가진 새로운 레코드가 삽입되면, h(K)를 수행한다.
      h(K)번째 bucket에 공간이 있으면, 이 버킷의 블록에 레코드를 삽입한다.
      만약 첫 번째 블록에 공간이 없으면 이 블록에 체인된 오버플로 블록 중 하나에 삽입한다.
      (체인된 블록이 없다면 새로운 오버플로 블록을 만들고 그곳에 삽입)


      https://www.lucidchart.com/publicSegments/view/e18f9fff-2fc4-494e-86b3-f130526d0310/image.png 




      Deletion from a Hash Table

      Deletion

      삭제도 같은 패턴이다.
      버킷의 h(K)번째로 가 K로 삭제할 레코드를 찾고, 지운다.
      만약 블록 사이에서 레코드의 이동이 가능하면 삭제 후
      체인을 한 개 줄여 통합할 수도 있다 .


      https://www.lucidchart.com/publicSegments/view/168471d7-5abb-497e-8fd5-7a70a7133921/image.png




      Efficiency of Hash Table Indexes

      이상적으로, 각 블록을 알맞게 담을만한 충분한 크기의 버킷들이라면,

      Key를 찾는데 1회의 Disk I/O, 
      Insert / Delete는 2회의 Disk I/O밖에 하지 않는다.

      이 횟수는 덴스/스파스/B-tree 인덱스 구조보다 현저히 낮은 숫자이다.
      하지만, 파일이 점점 커지면 특정 버킷에 체인된 블록들이 주렁주렁 달리는 사태가 발생할 것이다.
      따라서, 적어도 한 번의 Disk I/O가 발생하는 각 블록의 리스트가 아주 길어지면 좋을 것이다.
      그러므로 버킷 당 블록 수를 낮게 유지할 필요가 있다.

      우리는 지금껏 'Static Hash Tables(정적 해시 테이블)' 에 대해 살펴보았다. (버킷의 길이는 B로 고정)
      이번엔 'Dynamic Hash Table(동적 해시 테이블)' (거의 모든 버킷들이 버킷 당 블록 하나씩만 매핑되게 유도가능한 해시 테이블)
      에 대해 간단히 살펴보자.

      1. Extensible hashing : 해시 테이블이 너무 작다고 느껴질 때, 두배 늘리는 기법
      2. Linear hashing : 약간의 growing이 필요할 때, 매번 1씩 늘리는 기법



      Extensible Hash Table & Linear hash Table

      Extensible Hash Table(확장 해시 테이블)

      1. 버킷에 Indirection level 도입. 즉, 데이터 블록 자체를 직접 포함하는 배열 대신, 버킷을 참조하는 블록에 대한 포인터의 배열을 둔다.
      2. 포인터의 배열은 커질 수 있다. 그 길이는 항상 2배씩 커지며, 버킷 수도 2배씩 증가한다.
      3. 그러나, 각 버킷당 데이터 블록이 반드시 하나씩만 있어야 하는 건 아니다.
        -> 해당 버킷의 총 레코드 개수가 블록 안에 들어갈 수 있다면, 특정 버킷들은 블록을 공유할 수도 있다.


      Linear Hash Table(선형 해시 테이블)

      1. 선형 해싱은, 해시 테이블이 한 번에 한 개의 슬롯씩 증가시킨다.
      2. 슬롯 생성이 자주 일어나 효과적이지 않아 보이지만,
        이런 빈번한 싱글 슬롯 생성은 컬리젼 체인(Collision Chain) 길이를 조절하는 데 매우 효과적이다.






      마침.


      블로그 이미지

      차트

      소소한 일상 C코드 DB 항상 행복하게^-^★

      ,

      Define on SQL about kinds of Relations

      Database Modifications

      지금까지 결과를 출력하는 query 에 대해 알아보았는데, 
      이번에는 DB의 State(상태)를 바꾸는 여러가지 작업(Modification)들에 대해 알아보자. 

      1. Insert tuples into a Relation.
      2. Delete certain tuples from a Relation.
      3. Update values of certain components of certain existing tuples.

      이 세 가지를 'MODIFICATIONS' 라 부른다. 

      INSERTION

      삽입 연산으로, Relation에 tuple을 삽입한다. 

      INSERT INTO R(C1, C2, ..., Cn) VALUES (V1, V2, ..., Vn);
      
      R : Relation
      C : Column
      V : Value
      
      • Relation의 모든 columns를 포함하지 않으면, Default NULL 값이 입력된다. 
        (Default 값에 대해선 밑에서 다시 다루겠다.) 
      • 모든 column에 대해 하는 연산의 경우, List는 생략이 가능하다. 
        하.지.만 이 경우 column순서나 목록을 정확히 알아야 하기 때문에 주의가 필요하다. 
        정확히 알지 못하는 경우에는 일일이 명시하도록 하자. 

      예 : 

      Movie ( title, year, length, inColor, studioName )
      
      INSERT INTO Movie ( title, year ) VALUES ( 'THOR : Ragnarok', '2017' );
      
        -> Relation "Movie"에 title이 토르:라그나로크, year이 2017인 tuple을 삽입
           나머지 column ( length, inColor 등등 )의 값들은 Default인 NULL로 처리된다.
      
      • 또한 삽입할 tuple을 하위쿼리(subquery)로 작성할 수도 있다. 

      예 : 

      Movie ( title, year, length, inColor, studioName )
      Studio ( name, address )
      
      INSERT INTO Studio ( name )
           SELECT DISTINCT  studioName
           FROM  Movie
           WHERE  studioName NOT IN ( SELECT  name  FROM  Studio );
      
      "Movie"에는 있고, "Studio"에는 없는,
      다시말해 "Studio"에서 빼먹은 studio_name이 있으면 그것을 삽입
      

      DELETION

      삭제 연산으로, Relation으로부터 tuple을 삭제한다. 

      DELETE FROM R WHERE < condition >;
      
      R : Relation
      
      • INSERT 와는 다르게, 튜플을 지정할 순 없지만 WHERE절에서 조건을 줄 수 있다. 

      예 : 

      Students ( s_id, name, dept_name, grade )
      
      DELETE FROM  Students 
      WHERE  name = 'Bohyun' AND  dept_name = 'Comp. Sci.';
      
      Relation "Students"에서
      이름이 보현이고 학과가 컴퓨터공학인 tuple을 삭제
      

      UPDATE

      갱신 연산으로, Relation에 존재하는 tuple의 component를 수정한다. 
      흔히 '갱신'이라 함은 삽입/삭제와 같은 것을 생각할 수 있지만, 
      SQL에서의 Update는 따로 정의된 Modification이다. 

      UPDATE R SET < New-value assignments > WHERE < C >;
      
      R : Relation
      New-value assignment : R의 column을 원하는 New-value로 바꿈
      C : Condition 
      

      예 : 

      People ( name, age, gender )
      
      UPDATE  People SET  gender = 'M'
      WHERE name  LIKE  '%철수';
      
      Relation "People"에서 이름에 철수가 포함되면 성별을 남성으로 갱신
      
      • 갱신이 복잡할 경우, CASE 조건문(WHEN, THEN, ELSE, END)을 사용하여 
        갱신할 수 있다.

      예 : 

      table ( name, id, C1, C2 )
      
      UPDATE  table  SET  C1 = CASE id
      				WHEN 1 THEN 2
      				WHEN 3 THEN 4
      				ELSE C1
      				END,
      		        C2 = CASE id
      				WHEN 4 THEN 10
      				WHEN 9 THEN 20
      				ELSE C2
      				END
      	  	  WHERE  id IN ( ~  ( 생략 ) );
      
      Relation "table"에서 C1의 값을 id에 따라 갱신, C2의 값을 id에 따라 갱신     < Figure 1 참조 >
      

      https://www.lucidchart.com/publicSegments/view/34e6af56-633d-449d-8511-bc83031a2523/image.png

      • 또한 UPDATE DELETE & INSERT로 다루기도 한다.


      Defining a Relation Schema in SQL

      SQL에서 다루는 Relation schema가 어떻게 정의되는지 살펴보자. 

      1. Data Type : column을 이루는 Data types
      2. CREATE / DROP / ALTER : Table 생성, 삭제, 변경
      3. Defalut Value
      4. INDEX
      5. View : Virtual relation

      Data Types

      모든 columns는 Data type을 가지고 있어야 한다.

      1. Character strings ( fixed or varying length )
        • CHAR(n) : 길이 n을 갖는 문자열 ( 예 : CHAR(5)의 she -> 'she ' )
        • VARCHAR(n) : 최대길이 n을 갖는 문자열


      1. Bit strings ( fixed or varying length )
        • BIT(n) : 길이 n을 갖는 비트
        • BIT VARYING(n) : 최대길이 n을 갖는 비트


      1. Boolean type
        • BOOLEAN : TRUE, FALSE, UNKNOWN으로 나오는 타입


      1. Integer type
        • INT / INTEGER : 정수 값
        • SHORTINT : 작은 정수 값


      1. Floating-point numbers
        • FLOAT / REAL : 소수, 4byte
        • DOUBLE PRECISION : 소수, 8byte, 문자열로 저장
        • DECIMAL / NUMERIC(n, d) : n개의 10진수, d개의 소수


      1. Dates and Times
        • DATA
        • TIME
        • TIMESTAMP


      CREATE / DROP / ALTER

      CREATE

      • CREATE를 통해 simple하게 Relation schema를 선언할 수 있다. 
      • 괄호 뒤에 column list와 그들의 type도 선언한다. 

      예 : 

      CREATE  TABLE  Member  (
           name  VARCHAR ( 10 ),
           address  VARCHAR ( 50 ),
           gender  CHAR ( 1 ),
           birthday  DATE
      );
      
      Relation "Member"를 생성, column은 이름 / 주소 / 성별 / 생년월일
      

      DROP / ALTER

      • DROP : R은 더이상 Database의 schema가 아니게 되는, 완전한 삭제 연산 (Table을 통으로 삭제) 

      예 : 

      T1 ( C1, C2, C3 )                                      T2 ( C4, C5, C6 )
      T2 ( C4, C5, C6 )                  ->
      
      DROP  TABLE  T1;
      
      Relation "T1"을 삭제
      
      • ALTER : ( ADD / DROP ) column name (ADD의 경우 Data type도 선언) 

      예 : 

      Member ( name, address, gender, birthday )
      
      < ADD >
      ALTER  TABLE  Member  ADD  phoneNumber CHAR ( 13 );
      
      Relation "Member"에 휴대폰번호 column을 추가
      
      
      < DROP >
      ALTER  TABLE  Member  DROP  birthday;
      
      Relation "Member"에 생년월일 column을 제거
      

      Default Values

      • Tuple을 생성하거나 modify할 때, 모든 component를 명시하지 않을 때 도 있다. 
        이 문제를 해결하기 위해 SQL은 NULL 값을 제공한다. 즉, 기본적으로 Default값은 ' NULL ' 값이다. 
      • Data type 뒤에 DEFAULT를 사용하여 원하는 값을 Defalut값으로 지정할 수도 있다. 

      예 : 

      CREATE  TABLE  Member  (
           name  VARCHAR ( 10 ),
           address  VARCHAR ( 50 ),
           gender  CHAR ( 1 )  DEFAULT  '?',
           birthday  DATE  DEFAULT  '0000 - 00 - 00'
      );
      
      Relation "Member" 생성 시 Data type 뒤에 Default 값을 줌
      

      INDEX

      Why we use it?

      • 많은 Tuples을 전부 스캔하여 condition에 맞는, 몇 안되는 Tuples을 찾는 건 굉장히 expensive하다. 
        즉, Large Relations의 Find 효율성을 위해 INDEX를 부여함으로써 효율적으로 관리할 수 있다. 

      Definition of INDEX

      • 특정 Relation과 Logical한 연관을 두어, 해당 column을 기준으로 INDEX라는 object를 생성한다. 
      • 이러한 Index는 B-tree 등 다양한 기법들이 존재하지만, 우선은 기본적인 것들을 살펴보도록 하자.

      인덱스 생성하기

      CREATE INDEX Index_name ON R ( C );
      
      R : Relation
      C : column
      
      • 이 때, 1개 이상의 column으로 Index 구성 시, column 순서대로 우선순위가 결정되어 Order한다. 

      Selection of INDEXes

      • Index의 Selection은 DB designer에게 Trade-off를 요구하며 실제로 
        이 선택은 DB 설계가 acceptable한 지 영향을 미치는 중요한 요인 중 하나이다. 
      • 두 가지 고려사항은,
        1. Index의 존재는 Query속도를 크게 향상시키며 경우에따라 관련된 Join 또한 향상시킬 수 있다. 
        2. However, 모든 Index는 Modifications (Insertion, Deletion, Update)를 더 복잡하고 느리게 만든다. 
      • 그래서, Index selection은 DB 설계에서 가장 어려운 것 중 하나이다. 
        (modify가 자주 일어나면 그에따라 Index도 계속 바뀌어야 하기 때문) 
      • WHERE절에서 상수와 비교가 자주 일어나는 column이나, 
        Join condition으로 자주 언급되는 column으로 Index를 선택하는 게 유용하다. 
      • Modification 빈도수에 따라 Index 생성도 고려하는게 맞지만, 
        그럼에도 불구하고 Index 생성은 무척 효율적이다. (사실 Modification 자체도 굉장히 조심스럽게 해야함) 
      • Index 사용이 full-scan 보다 훠어어얼씬 더 빠르다. 


      View

      Why & Definition

      • Table을 만들면 그 Table은 modification하지 않는 한 
        변화가 없는상태로 물리적 공간 지속적으로 저장되어 있다. 
      • View라는 Relation은 물리적으로 존재치않고 query같은 expression으로 정의된다. 
        물리적으로 존재하지 않아도 마치 물리적으로 존재하는 것처럼 query받을 수 있으며 
        일부는 modify도 할 수 있다. 

      VIEW 생성하기

      CREATE VIEW < view-name >  AS  < view-definition >;
      
      Q : Query. It is Definition of View.
      

      Querying Views

      • View가 참조하는 Relation 내용이 바뀌면, View도 자동으로 갱신된다. 


      Relations, Tables, and Views 

      Relation을 구별하기 위해 물리적 : table, 가상 : view로 구분하여 사용한다. 
      또한 stored되는 Relation임을 강조하고 싶을 땐 'Base Relation'또는 'Base Table' 이라 나타낸다. 
      View는 영구히 저장되는 것도 아니고, 단지 subquery에 사용되는 일시적인 결과물일 뿐이다. 
      그래도, Relation 취급을 한다. 


      • Renaming View
        • Query에서 View의 column명을 본래의 Table과 구별하기 위해 Renaming 또한 가능하다. 

      예 : 

      T1 ( C1, C2 )
      
      CREATE VIEW V1 ( A1, A2 ) AS 
           SELECT  C1, C2
           FROM  T1;
      
      V1 ( A1, A2 )
      
      Relation "T1"을 참조한 View "V1"은, column 이름이 T1의 column과 다르지만, 그 안의 값은 같음
      


      Modifying Views

      일단, view에 modify한다는 건 의미가 이상하다. 
      왜? 이녀석은 단지 참조하는 가상의 Relation일 뿐인데? 
      Insert의 경우 그 데이터는 어디에 저장되는가? 

      Answer is -> '할 수 없다' 이다. 

      하지만, SQL에서의 Updatable View라는(일반 view는 불가) View는 modify 가능하다. 
      View에 modify를 행하면, 그 결과 역시 Base Table에도 적용 된다. 

      View modify에 있어 중요한 두 가지는, 

      1. WHERE clause는 subquery에서 R을 포함하면 안된다. 
      2. SELECT clause의 column list는 view에 삽입된 모든 tuple을 가질만한 충분한 column이 포함되어야 한다. 
        그래야지만 다른 column에 NULL로 채운다던가, 삽입된 view의 tuple을 base relation으로 가져온다던가 할 수 있다. 
      Students ( ID, NAME, AGE, DEPT, GRADE )
      
      CREATE  VIEW  V1  AS
         SELECT  NAME, AGE, DEPT
         FROM  Students
         WHERE  GRADE ≥ 3;
      

      https://www.lucidchart.com/publicSegments/view/2cbfdf05-8b68-42ea-86a9-5e4df8b862b0/image.png

      • Insert

      예 : 

      INSERT  INTO  V1
      VALUES  ( '민남기', 30, 'Business Ad' );
      

      https://www.lucidchart.com/publicSegments/view/a49e68df-e6d9-47d4-be94-e9961b5da63b/image.png

      • Update

      예 : 

      UPDATE  V1
      SET  DEPT = 'Legacy'
      WHERE  AGE ≥ 25;
      

      https://www.lucidchart.com/publicSegments/view/633a647b-b7a1-40de-8d22-a93e8c71c60e/image.png

      • Delete

      예 : 

      DELETE  FROM  V1
      WHERE  DEPT = 'Legacy';
      

      https://www.lucidchart.com/publicSegments/view/c2efe44c-8880-43ed-96ff-7a248db31217/image.png

      • DROP ( table과 같지만 굳이 언급한 이유는, View가 Updatable이든 아니든 상관없이 가능하기 때문 )

      예 : 

      DROP  VIEW  V1;
      





      DROP Is Powerful Modification 

      앞서 설명했듯 DROP은 Table에도 가능한데, 이 Table에서 참조된 View가 있다면 
      그 View마저도 사용불가로 만든다. (물리적 저장을 하지 않았기 때문)






      Why Some Views Are Not Updatable? 

      두 개의 relation에서 생성한 view같은 경우, 
      Insert하면 빈값들은 NULL이 될텐데, Join연산에서 
      NULL끼리의 연산은 불가하기 때문에 insertion 자체가 불가능하다.







      마침.


      블로그 이미지

      차트

      소소한 일상 C코드 DB 항상 행복하게^-^★

      ,