

이번 chapter에서는 database가 data를 저장하는 방법과 data를 요청했을 때 data를 찾는 방법을 배운다.
개발자는 data storage engine를 직접 구현하지 않고 선택하여 쓴다. 자신이 개발하려는 application에 맞는 engine을 선택하려면 storage engine이 어떻게 작동하는지 대략적인 지식이 있어야 한다. 예를 들어서, transaction workload에 최적화 된 engine과 analytics에 최적화 된 engine에는 큰 차이가 있다.
가장 먼저, RDB와 NoSQL 같은 친숙한 데이터베이스에 대한 storage engine에 대해서 소개를 한 후, log-structured storage engine과 page-oriented storage engine 두가지에 대해 살펴본다.
log를 사용한다. index라고 하는 data structure가 필요하다.Hash map은 key-value data를 구현할 때 사용된다. inmemory data structure에서 쓰이는 hash map을 disk에 있는 data의 index로 쓰는 방법은 다음과 같다.
data storage는 간단하게 file을 추가하는 것으로만 구성된다고 하자. key를 data file의 byte offset에 맵핑시켜서 in-memory hash map을 구성한다. 파일에 새로운 key-value 쌍을 추가할 때마다 hash map을 update하여 byte offset을 update 해줘야 한다. value를 찾으려면 data file에서 byte offset에 있는 값을 위치로 하여 읽으면 된다.

이런 storage engine 방식은 각 key의 value가 자주 update 되는 상황에서 적합하다.
value가 자주 update 됨에 따라 file을 계속 추가하기만 하면 disk의 공간이 부족해진다는 문제가 발생한다.
disk 공간 부족을 방지하기 위해 특정 크기의 segment로 log를 분할하는 방법을 사용한다.
특정 크기에 도달하면 segment를 닫고 새 sement file에 subsequent write를 수행한다. 그 후, segment를 압축하는데, 여기서 압축이란 아래 그림처럼 log에서 중복된 key를 버리고 각 키에 대한 최신 update만 유지하는 것이다.

압축을 수행하면서 동시에 여러 segment를 병합할 수도 있다. 병합된 segment는 새 파일에 작성된다. 고정된 segment의 병합 및 압축은 background thread에서 실행될 수 있기 때문에 수행 도중 read/write 요청이 들어오더라도 이전 segment를 사용하여 정상적으로 처리가능하다. 수행이 완료되면 새로 병합된 segment를 사용하고, 이전 segment는 삭제한다.

이 방식을 구현하는데 고려해야 할 문제들이 있다.
append-only의 장점
hash table의 단점
SST (Sorted String Table) : segment file의 key-value 쌍을 key를 기준으로 정렬 (각 key는 merge 된 segment file 내에서 한 번만 나타나야 한다.)
SST는 Hash index를 가진 segment file 보다 몇 가지 장점을 가진다.
가용 메모리보다 file이 더 크더라도 segment를 merge하는 것이 더 간단하고 효율적이다. (merge sort 알고리즘과 같은 방식을 사용한다. 아래 그림에서 숫자를 따라가면서 보면 잘 이해될 것이다.)

file에서 특정 key를 찾기 위해 메모리에 모든 key의 index를 저장하고 있을 필요가 없다. (key를 기준으로 정렬되어 있으므로, 해당 메모리에 저장되어 있지 않은 key를 찾으려면 그 key보다 사전순으로 앞에 있는 key의 offset으로 이동해서 찾으면 된다.) 하지만 일부 key는 저장 할 필요가 있다.

read request의 범위 내에서 여러 레코드들을 block 으로 group화하고, disk에 쓰기 전에 압축할 수 있다. index의 각 entry는 block의 시작 위치를 가르킨다. 이런 과정을 통해 디스크 공간을 절약할 수 있고, I/O bandwidth 사용도 줄일 수 있다.
memtable이라고 한다.LSM-tree : Log-Structured Merge-Tree Lucene은 Elasticsearch나 Solr에서 사용하는 full text search engine이다. Lucene은 term dictionary를 저장하기 위해 비슷한 방법을 사용한다.
search query에서 단어가 주어지면, 그 단어가 적힌 모든 document (web page ... )를 찾는다. 이를 위해서 key는 word(term), value는 word를 내포한 모든 document의 ID list (
posting list)로 정한다.
LSM tree algorithm은 database에 존재하지 않는 key를 찾는 경우에 느리다는 단점이 있다. memtable 부터 가장 오래된 segment까지 뒤져야 하기 때문이다.
Bloom filter를 추가적으로 사용한다. 이것을 사용하면, key가 database에 존재하지 않을 때 알려준다.SST를 compact 하고 merge하는 순서와 타이밍을 결정하는 전략
size-tiered와 leveled compaction을 사용.LSM tree의 장점 (LSM tree의 기본 개념은 background에서 cascade하게 SST를 지속적으로 merge하는 것)
dataset이 가용 메모리보다 훨씬 크더라도 효과적
data가 정렬된 순서로 저장돼 있으면 range query가 효과적임
높은 write throughput 보장
B-Tree는 가장 보편적으로 쓰이는 indexing structure이다.
SST처럼 key를 기준으로 정렬된 key-value쌍을 가지기 때문에 key-value 검색과 range query에서 효율적이다.
log structured index는 database를 variable size를 가진 segment로 나누고 항상 sequential하게 segment를 기록한다. 하지만, B-Tree는 4KB의 고정된 크기 block이나 page로 나누고 한 번에 하나의 페이지에 write/read를 한다.
B-Tree index에서 key를 검색
root로 지정되기 때문에 index에서 key를 찾으려면 root에서 시작해야 한다. 최종적으로는 leaf page를 포함하는 page에 도달한다. 이 leaf page는 각 key의 value를 포함하거나 값을 찾을 수 있는 page의 reference를 포함한다.branching factor라고 한다. 보통 한 page의 branching factor는 수백 개이다.
balance를 유지한다. n개의 key를 갖는 B-tree의 depth는 항상 O(logn)이다. 보통 database의 B-tree depth는 3이나 4이면 충분하다.orphan page(어떤 page와도 부모 관계가 없는 page)가 발생할 수 있다. write-ahead log 혹은 redo log를 추가해서 스스로 복구되게 끔 한다.latch (lightweight lock)으로 해결할 수 있다.page를 overwrite하고, WAL을 유지하는 대신 copy-on-write scheme을 사용한다.
전체 key를 저장하지 않고 key를 축약해서 저장한다. 이때 tree 내부 page에서 key range 사이의 경계에 대한 충분한 정보를 제공할 수 있는 정도이면 된다. 혹은 branching factor 수를 늘려서 tree의 depth를 낮춘다.
leaf page를 disk 상에 연속된 순서로 나타날 수 있게 배치한다. 하지만 tree가 커지면 순서를 유지하기 어렵다.
leaf page에서 각 양쪽 sibling page에 대한 refernce를 가지는 등의 pointer를 추가한다. -> 상위 page로 이동하지 않고 순서대로 스캔 가능
fractal tree : log structure 개념을 일부 도입한 B-tree 변형
write amplification (<- 이것은 SSD의 수명과 밀접한 연관)key-index의 예시를 살펴보자.
primary keysecondary indexCREATE INDEX 명령어를 사용하여 같은 테이블에 대한 secondary index를 여러 개 생성할 수 있다. heap file (append only)이라고 한다.clustered indexcovering index 혹은 index with included columnscover했다" 라고 함.)multiple column에 대해 query할 때는 concatenated index를 사용해야 한다.
한 번에 여러 column에 대해 query할 때 일반적으로 쓰이는 방법이 multi-dimensional index이다.
similar key에 대해서 query를 날리는 것을 fuzzy query라고 말한다. inmemory database가 개발되었다.transcation 이라는 용어는 logical unit으로서 read와 write 그룹을 나타내고 있다.
transaction processing은 클라이언트가 low latency read/write를 가능하게 한다는 뜻이다. 즉, 반드시 ACID 속성을 가질 필요는 없다.online transaction processing(OLTP)database는 data analytics에서도 많이 사용되고 있다.
online analytic processing (OLAP)
OLTP 시스템들은 분석 목적으로 사용되지 않고 개별 database에서 분석을 수행할 때 사용되는 경향을 보인다. 여기서 개별 database를 data warehouse라고 한다.
OLTP는 대개 사업 운영에 중요하기 때문에 높은 가용성과 낮은 지연 시간의 transaction process를 기대한다. 하지만 데이터베이스 관리자는 OLTP database에 ad hoc analytic query를 실행하는 것을 꺼려한다.
data warehouse는 OLTP 작업에 영향을 주지 않고 마음대로 query를 날릴 수 있는 개별 database이다.
data warehouse는 회사의 모든 다양한 OLTP 시스템의 read-only 복사본이다.
데이터는 아래 그림처럼 OLTP 데이터베이스에서 추출되고, 분석 친화적인 schema로 변환된 뒤, 깨끗하게 정리되어 data warehouse에 load된다.
Extract-Transform-Load(ETL)이라고 한다.
transaction processing에는 다양한 data model이 있지만, analytics에는 data model의 다양성이 적다.
많은 data warehouse는 dimensional modeling이라고 불리는 star schema를 많이 사용한다.
schema 중심에는 특정한 시간에 발생한 이벤트를 난타내는 fact table이 있다. (아래 그림에서의 fact_sales table)
보통 fact는 개별 event를 나타낸다. 분석의 유연성을 극대화할 수 있기 때문이다.
fact table의 일부 column은 property 이다. (아래 그림에서의 제품 가격, 공급자가 지불한 비용 ....)
어떤 column들은 dimension table이라고 불리는 다른 table을 참조하는 foreign key이다.
fact table의 각 row는 이벤트를 나타내고, dimension은 이벤트의 속성인 who, when, where, what, how, why를 나타낸다.

star schema라는 이름은 dimension table을 사용해 표현한다. (dimension table을 사용해서 분석이 가능함 ex. 휴일과 평일의 판매 차이)
star schema의 변형이 snowflake schema이며, 차원이 더 세분화된다.
fact table에 row와 data의 개수가 많을 때 효율적으로 query를 날리고 data를 저장하는 방법
대부분의 OLTP database에서 storage는 row-oriented 방식으로 data를 배치한다.
대신 column-oriented storage 방법을 사용한다.
column-oriented storage : 모든 value를 하나의 row에 저장하지 않고, 각 column을 개별 파일에 저장하고 query에 사용되는 column만 읽음.
아래 사진은 그림3-9의 예제를 column-oriented storage로 구성한 것이다. 각 column file에 포함된 row가 모두 같은 순서로 배치되어 있다.

그림 3-10을 보면 각 column에서 같은 값이 반복해서 나온다. 이럴 경우 compression을 하여 disk throughput을 줄일 수 있다. compression 기법은 data에 따라 다양한다.
bitmap encoding 기법 : n개의 distinct value를 가지고 와서 n개의 bitmap으로 만든다. distinct value 하나가 bitmap이고, 각 row는 한 bit를 가진다. 만약 row가 해당 값을 가지면 bit는 1이고, 그렇지 않으면 0이다.

- 만약 n이 매우 작으면, row 당 하나의 bit로 저장할 수 있다.
- 만약 n이 크면 (`sparse`), bitmap을 추가적으로 run-length encoding 한다. (위 사진의 하단)
vectorized processing : bit AND와 OR 연산을 압축된 column data chunck에 바로 연산하는 기법several differen way로 정렬해서 복제해두자! -> query를 처리할 때 qeury pattern에 가장 적합한 버전을 사용할 수 있다.column-oritented storage, compression, sorting은 read query에는 적합하지만, write를 어렵게 만든다.
정렬된 table의 중간 row에 insert를 하려면 모든 column file을 재작성해야 한다.
LSM tree를 이용해서 해결할 수 있다 : 모든 write는 in-memory store로 이동해 sorted structure에 추가되고, disk에 쓸 준비를 한다. write가 충분하게 모였으면, disk의 column file에 merge하고 대량으로 새로운 file에 기록한다.
Vertica가 위와 같은 방법을 사용한다.
data warehouse query에는 aggregate function이 있다 : COUNT, SUM, AVG, MIN, MAX
materialized view를 사용해서 query가 자주 사용하는 COUNT나 SUM을 cache 해보자! relational data model에서는 이런 것을 standard (virtual) view라고 정의한다.
materialized view는 디스크에 기록된 query result의 복사본이다.
원본 data를 변경하면 materialized view를 update 해야 한다. (update 과정은 cost가 비싸기 때문에 OLTP DB에서는 자주 사용하지 않는다.)
materialized view를 사용한 것이 data cube 혹은 OLAP cube이다.

materialized data cube의 장점은 특정 query를 미리 계산했기 때문에 해당 query를 수행할 때 매우 빠르다는 것이다.
단점은 유연성이 없다는 것이다.
따라서 대부분의 data warehouse는 가능한 한 많은 raw data를 유지하려고 하며, 특정 query에 대한 성능 향상에만 사용한다.