Dremel: Interactive Analysis of Web-Scale Datasets
컬럼 저장과 서빙 트리로 웹 스케일 데이터를 대화형으로 분석하기
- Haram Lee
- 2026-08-17
- studies / Papers
Dremel: Interactive Analysis of Web-Scale Datasets
Melnik et al., VLDB 2010
https://vldb.org/pvldb/vol3/R29.pdf
BigQuery는 쿼리 시간을 어디에서 줄이는가?
- 얼마나 적게 읽는가?
- 읽은 것을 얼마나 많이 병렬화하는가?
- 중간 결과를 얼마나 빨리 줄이는가?
- 느린 작업 하나가 전체 속도에 얼마나 덜 영향을 미치게 하는가?
1. INTRODUCTION
text
┌── Dremel ─────── Interactive analysis
│
MapReduce ─── GFS ── Shared columnar data
│
├── FlumeJava ─── Complex analysis
│
└── Pipeline ──── Production- Dremel: read-only nested data를 분석하기 위한 확장 가능하고 대화형인 애드혹 쿼리 시스템
- 다단계 실행 트리
- column-oriented data batch
- in situ에 있는 중첩 데이터를 직접 처리 → GFS같은 분산 파일 시스템이나 bigtable같은 다른 저장 계층에 놓인 데이터에 그대로 접근할 수 있음
- 데이터 응답 시간 → 데이터 탐색 / 모니터링 / 온라인 고객 지원 / 빠른 프로토타이핑 / 데이터 파이프라인 디버깅
- 병렬성
- 한 클러스터에는 자원을 공유하는 여러 분산 애플리케이션이 함꼐 배치됨
- 각각의 워크로드와 하드웨어 사양도 다름
- 분산 애플리케이션의 어느 한 워커는 같은 작업을 실행하는 다른 워커보다 훨씬 오래 걸릴 수 있고, 장애나 클러스터 관리 시스템의 선점 때문에 완료하지 못할 수도 있음
- → 빠른 실행과 내결함성을 확보하려면 straggler와 장애 처리가 필수적
- 유연한 데이터 모델이 중요
- 정규화했다가 다시 결합하는 비용이 너무 비쌈
- Serving tree
- 분산 검색 엔진에서 사용
- 쿼리는 트리 아래로 전달되며 각 단계에서 재작성 → 하위 단계에서 받은 응답을 집게해 최종 결과를 조립함
- Ad-hoc 쿼리를 표현하기 위한 고수준의 SQL 유사 언어를 제공
- unlike Pig/Hive → 쿼리를 MR 작업으로 번역하지 않고 직접 실행
- Column-striped 저장 표현 사용
- 보조 저장 장치에서 읽는 데이터 양 감소
- 압축 비용이 저렴해짐
2. BACKGROUND
- e.g. 웹 페이지에서 새로운 종류의 신호를 추출
- 입력 데이터를 처리하는 MR 작업 → 해당 결과로 얻은 새로운 신호 수십억 개를 레코드 형태로 분산 파일 시스템에 저장 → 실험 결과를 분석하기 위해 다음 대화형 명령을 수행
text
DEFINE TABLE t AS /path/to/data/*
SELECT TOP(signal1, 100), COUNT(*) FROM tflowchart LR
A["Web Pages"]
B["MapReduce<br/>Signal Extraction"]
C[("Distributed File System<br/>(GFS)")]
D["Dremel<br/>Interactive Analysis"]
E["FlumeJava<br/>Deep Analysis"]
F["Continuous<br/>Pipeline"]
G["SQL<br/>Aggregation"]
H["Interactive<br/>Dashboard"]
I["Dataset<br/>Catalog"]
A --> B
B --> C
C --> D
D -->|"Irregularity found"| E
E -->|"Fix & validate"| F
F --> G
G --> H
F --> I
C -.-> J["Shared Storage Layer"]
J --> K["Shared Columnar Format"]
K --> L["Read Only<br/>Required Fields"]3. DATA MODEL
- Protocol Buffers
- 흠…
⭐ 4. NESTED COLUMNAR STORAGE
- goal: 검색 효율을 높이기 위해 특정 필드의 모든 값을 연속해서 저장하는 것
4.1. Repetition and Definition Levels
- 값만으로는 레코드 구조를 알 수 없음
- 반복 필드의 값 두 개가 주어져도 어느 수준에서 값이 반복되었는지 = 서로 다른 두 레코드에서 온 것인지, 같은 레코드 안의 반복값 두 개인지
- 선택 필드가 누락되었을 때 무엇이 명시적으로 정의되어 있었는지 알 수 없음.
- → Repetition Level / Definition Level
text
Document
├─ DocId required
├─ Links optional
│ ├─ Backward repeated
│ └─ Forward repeated
└─ Name repeated
├─ Language repeated
│ ├─ Code required
│ └─ Country optional
└─ Url optional1. Repetition level
- field의 path에 있는 repeated field 중 어디에서 값이 반복되었는지
- e.g.
text
Name repeated
└ Language repeated
└ Code required- repetition level: 0~2
- r1
text
Name 1
├ Language
│ └ Code = "en-us"
├ Language
│ └ Code = "en"
Name 2
└ Language 없음
Name 3
└ Language
└ Code = "en-gb"text
value repetition level
en-us 0 -> 새로운 record 시작
en 2 -> Langauage에서 반복 발생, r=2
NULL 1
en-gb 1 -> language가 아니라 새로운 Name으로 넘어감- 문제: 두 번째 Name에는 Language 자체가 없으므로, code column을 단순히 en-us, en, en-gb라고 저장하면 en-gb가 Name2에 속한 건지 3에 속한 건지 구별하기 어려움.
- ⇒ Null을 넣음 = Name은 존재했으나 Code까지 내려가지는 않았다
- ⇒ 그러나 Name/Languge/Country중 어느 것이 없었는지 알기 어려움
2. Definition Level
- 해당 leaf field까지 가는 path에서 optional/repeqted field가 어디까지 실제로 존재했는지
text
Name repeated
└ Language repeated
└ Country optional- maximum definition level = 3
- is-null bit 대신 integer definition level을 사용
- ⇒ leaf column만 보더라도 parent field들이 실제로 존재했는지를 알 수 있음
text
value r d
us 0 3
NULL 2 2
NULL 1 1
gb 1 3
NULL 0 1⇒ us, r=0, d=3 → 새로운 document, Name/Language/Country 모두 존재하고 Country는 “us”
이런 식으로 해석이 가능하다…
- NULL 값을 실제 value stream에 저장할 필요가 없음
- ⇒ 당연함!! definition level을 보면 이미 NULL인지 알 수 있기 떄문
- level도 작은 정수니까 bit packing 저장 가능
text
Nested records
↓
그냥 column으로 split하면 구조을 알 수 없음 -> 어디까지?
↓
r / d level로 구조만 아주 작게 따로 encoding
↓
전체 row를 읽을 필요 없이
필요한 leaf columns만 읽을 수 있음
↓
I/O 감소4.2. Splitting Records into Columns
- r과 d를 실제로 어떻게 빠르게 계산해서 column을 만들지?!
- ⇒ 기본적으로 recode 구조를 재귀적으로 따라가면서 각 field value의 repetition/definition level을 계산
- problem) data는 sparse할 수 있음 = field가 수천 개 있더라도 실제 한 record에서는 전부 사용되지 않을 수도 있음.
- Field Writer Tree
text
Document writer
/ \
Links writer Name writer
/ \ / \
Backward Forward Language Url
/ \
Code Country- 이렇게 해놓고 각 leaf writer가 자기 column에 데이터를 씀
- 값이 없는 child까지 매번 업데이트하지 않음 / 부모의 level 상태를 inherit하고 있다가, 실제로 자기 값이 들어올 때만 부모와 동기화
- ~= lazy propagation
4.3. Record Assembly
- column들만 보고 원래 nested record를 어떻게 빠르게 조립하지?
- 4.2절과 반대
- FSM (=Finite State Machine)
- 지금 column에서 값을 하나 읽었으면 다음에는 어느 column을 읽어야 하는지 FSM이 결정
- 헐… 오토마타의 그것. 대충알겠당
- 모든 field를 복원할 필요가 없음. 요청에 댑하기만 필요한 field만 사용하는 FSM을 만듦
5. QUERY LANGUAGE
- 가볍게 읽을 것!!
- Dremel의 언어는 SQL 기반인데 입출력은 둘 다 nested table임
- e.g.
sql
SELECT
DocId AS Id,
COUNT(Name.Language.Code) WITHIN Name AS Cnt,
Name.Url + ',' + Name.Language.Code AS Str
FROM t
WHERE REGEXP(Name.Url, '^http') AND DocId < 20;- Path Expression
- Name.Language.Code같은 nested path를 별도 join이나 flatten 없이 그대로 참조
- 저장도 leaf path 단위로 했으니까 쿼리할 때도 그대로 처리
- WHERE
- 조건을 만족하지 않는 nested branch를 잘라낸다(prune)
- selection on a nested record = pruning branches of a labeled tree
- Projection
- 어느 nesting level에서 값을 만들지
- SELECT expression에서 가장 깊게 반복되는 input field와 같은 nesting level에서 output value를 만듦
- WITHIN
- nested record 내부 aggregation
COUNT(Name.Language.Code) WITHIN Name- nested aggregation 결과도 nested structure를 유지
e.g.
text
Document
└─ Name 1
├─ Language
│ └─ Code = en-us
└─ Language
└─ Code = en이면
text
COUNT(Code) WITHIN Name
→ 2다른 Name이
text
Name 2
└─ Language 없음이라면
text
COUNT(Code) WITHIN Name
→ 0결과는
text
Document
└─ Name
└─ Cnt = 2
└─ Name
└─ Cnt = 0- 흠. 글쿤
- 이런것도 지원
- nested subqueries
- inter-record aggregation
- intra-record aggregation
- top-k
- joins
- user-defined functions
⭐ 6. QUERY EXECUTION
- 그 SQL을 수천 대의 서버가 어떻게 나눠서 몇 초 안에 실행하느냐?
- 쿼리를 한 서버가 처리하지 않고 multi-level serving tree로 내려보냄
text
Client
│
▼
Root Server
│
▼
Intermediate Servers
│
▼
Leaf Servers
│
▼
Storage Layer (e.g. GFS)- Root: 쿼리를 받고 전체 작업을 나눔
- Intermediate: 하위 결과를 중간 집계
- Leaf: 실제 데이터를 읽고 scan/aggregation 수행
- Storage: columnar data가 저장돼 있음
- e.g.
sql
SELECT A, COUNT(B)
FROM T
GROUP BY A- Root server: T라는 table이 어떤 tablets로 구성되어 있지?!
- query rewriting: 각 서버가 자기 partition만 가지고 partial aggregation을 하게 함
- leaf가 실제 tablets을 병렬로 scan한 뒤 intermediate server가 partial result를 aggregate함 (→ root가 다 처리하면 완전 병목이 되겠지,,,)
- aggregation도 계층적으로 병렬화한다!!
- Columnar Storage → 적게 읽고
- Serving Tree → 빨리 나눠 읽고
- Query Dispatcher
- multi-user system: 동시에 여러 쿼리가 들어옴
- 그래서 query dispatcher가 뭘 하냐면,,,
text
1. Query scheduling
2. Priority handling
3. Load balancing
4. Straggler / failure handling- Slot
- 1 slot = leaf server 하나의 execution thread
- e.g. 3000 leaf servers × 8 threads/server = 24,000 slots
- 데이터를 tablet으로 쪼개놓고, 많은 slot이 동시에 처리한다
- straggler
- 오래 걸리면 기다려야 함
- dispatcher: 각 tablet 처리 시간을 histogram으로 모니터링
- → 같은 tablet을 multiple times redispatch할 수 있음
- tail latency 줄이기 위해서!
- Replication
- tablet 데이터는 여러 replica를 가지고 있음(보통 3-way)
- fault tolerance / query latency
- 최소 몇 %의 tablets를 scan한 뒤 결과를 반환할 것인지를 지정하는 parameter
- 꼭 다 읽어야 하냐? → 그렇지 않을수도…
- trade-off
- approximate analysis라면 꽤 좋을수도?
- leaf에서는 column block을 prefetch함
- asynchronously prefetch
- I/O latency를 compute와 겹치는 개념? 인듯
- execution tree
- physical query execution plan에 대응
- scalar expression 처리
- project-select-aggregate query: input columns를 lockstep으로 scan하면서 aggregate / scalar function 결과를 직접 만듦
- record assembly를 아예 bypass할 수 있음
- 흠…
- 저장만 columnar한 게 아니라 execution까지 column-oriented하게 할 수 있다
- 그래서 왜 빠르냐면
- Massive Parallel scan
- Hierarchical aggregation
- tail-latency mitigation
- column-oriented execution
text
Storage
│
├─ Columnar layout
│ → Read fewer bytes
│
├─ r/d levels
│ → Keep nested structure cheaply
│
▼
Execution
│
├─ Thousands of leaf servers
│ → Massive parallelism
│
├─ Multi-level serving tree
│ → Parallel aggregation
│
├─ Direct column processing
│ → Avoid record assembly
│
└─ Redispatch + replication
→ Reduce tail latency7. EXPERIMENTS
- Columnar storage → 훨씬 적은 I/O
- Dremel execution → minutes → seconds
- 노드 수 증가 시 거의 선형적으로 빨라짐!! < 헐…
- Serving tree가 큰 aggregation에 효과적
- Straggler가 tail latency의 주요 원인
- 실험을 했다 잘 했 다
- 아무튼 빠름
8. OBSERVATIONS
- Trillion-row scan도 interactive speed 가능
- Columns / servers에 대해 near-linear scalability
- Columnar storage는 MR에도 유효
- Record assembly / parsing은 비싸다
- MR과 Dremel은 대체재가 아니라 complementary
- 마지막 몇 %의 straggler가 어렵다!!