Workspace IndexAlgorithms › Day 76

Column Stores and Vectorized Execution (OLAP) TODO

Algorithms · Day 76 / 100 · E. Data & Storage Engines (Day 69-81)

Concept

A row store appends whole records together, while a column store lays out values of the same column contiguously, so a query only reads the columns it needs and I/O drops sharply. Because values that land together share the same type and often a similar distribution, compression schemes like RLE, dictionary encoding, delta encoding, and bit-packing work much better, and it becomes possible to operate directly on compressed data. The execution engine also processes data in batches of thousands of values (vectors) instead of the tuple-at-a-time Volcano model, cutting function-call overhead and exploiting cache locality and SIMD. The tradeoff is that single-row lookups and frequent updates suffer, so OLTP paths still belong on row stores.

Throwing analytical queries straight at an operational database is often tens of times slower and more expensive, and once the workload shape and storage layout are mismatched, index tuning alone won't recover it.

Code & Formula

# 컬럼 스토어와 벡터화 실행(OLAP) — 로우 스토어와 컬럼 스토어의 I/O·압축 차이를 흉내낸다.
# 컬럼별로 값을 모으면 RLE 압축이 잘 듣고, 한 컬럼만 읽어도 되는 질의에서 I/O 가 줄어든다.

rows = [
    {"country": "KR", "amount": 100},
    {"country": "KR", "amount": 120},
    {"country": "US", "amount": 90},
    {"country": "US", "amount": 95},
    {"country": "US", "amount": 80},
]

# 로우 스토어: "amount 합계"를 구하려면 레코드 전체를 훑어야 한다.
row_store_bytes_touched = sum(len(r) for r in rows) * 8  # 필드 전부를 스캔한다고 가정

# 컬럼 스토어: amount 컬럼만 연속 배치로 저장 → 그 컬럼만 읽으면 된다.
column_store = {
    "country": [r["country"] for r in rows],
    "amount": [r["amount"] for r in rows],
}
column_store_bytes_touched = len(column_store["amount"]) * 8  # amount 컬럼만 스캔

def run_length_encode(values):
    out = []
    for v in values:
        if out and out[-1][0] == v:
            out[-1][1] += 1
        else:
            out.append([v, 1])
    return out

# 벡터화 실행: 튜플 하나씩이 아니라 컬럼 배열 전체에 한 번에 연산(sum)을 적용한다.
def vectorized_sum(col):
    return sum(col)  # 실제 엔진은 SIMD 로 배치 처리하지만, 여기선 개념만 시연

country_rle = run_length_encode(column_store["country"])
total_amount = vectorized_sum(column_store["amount"])

print("row store bytes touched for SUM(amount):", row_store_bytes_touched)
print("column store bytes touched for SUM(amount):", column_store_bytes_touched)
print("country column RLE:", country_rle)          # [['KR', 2], ['US', 3]]
print("vectorized SUM(amount):", total_amount)      # 485

Exercise

Load the same event data into Postgres and DuckDB, then compare the scanned data volume and runtime of a large-scale aggregation query to confirm the effect of column pruning.

Practical Connection

Verex's analytics backend — indexing chain event logs to compute trading volume, open interest, and per-user P&L — is a textbook OLAP workload, so it's natural to separate the storage used for mirroring on-chain state from the storage used for analytics.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

컬럼 스토어와 벡터화 실행(OLAP) TODO

Algorithms · Day 76 / 100 · E. 데이터·스토리지 엔진 (Day 69–81)

개념

로우 스토어가 레코드를 통째로 붙여 저장하는 반면 컬럼 스토어는 같은 컬럼의 값을 연속 배치해, 질의에 필요한 컬럼만 읽어 I/O를 크게 줄인다. 같은 타입에 값 분포도 비슷한 데이터가 모이므로 RLE, 딕셔너리, 델타, 비트팩킹 같은 압축이 훨씬 잘 듣고 압축된 상태로 연산하는 것도 가능해진다. 실행 엔진도 튜플 하나씩 넘기는 volcano 모델 대신 수천 개 값 묶음(벡터) 단위로 처리해 함수 호출 오버헤드를 줄이고 캐시 지역성과 SIMD를 살린다. 대신 단건 조회나 잦은 갱신에는 불리하므로 OLTP 경로는 여전히 로우 스토어가 맞다.

분석 질의를 운영 DB에 그대로 던지면 수십 배 느리고 비싸며, 워크로드 성격과 저장 방식을 맞추지 못하면 인덱스 튜닝으로는 회복되지 않는다.

코드 · 수식

# 컬럼 스토어와 벡터화 실행(OLAP) — 로우 스토어와 컬럼 스토어의 I/O·압축 차이를 흉내낸다.
# 컬럼별로 값을 모으면 RLE 압축이 잘 듣고, 한 컬럼만 읽어도 되는 질의에서 I/O 가 줄어든다.

rows = [
    {"country": "KR", "amount": 100},
    {"country": "KR", "amount": 120},
    {"country": "US", "amount": 90},
    {"country": "US", "amount": 95},
    {"country": "US", "amount": 80},
]

# 로우 스토어: "amount 합계"를 구하려면 레코드 전체를 훑어야 한다.
row_store_bytes_touched = sum(len(r) for r in rows) * 8  # 필드 전부를 스캔한다고 가정

# 컬럼 스토어: amount 컬럼만 연속 배치로 저장 → 그 컬럼만 읽으면 된다.
column_store = {
    "country": [r["country"] for r in rows],
    "amount": [r["amount"] for r in rows],
}
column_store_bytes_touched = len(column_store["amount"]) * 8  # amount 컬럼만 스캔

def run_length_encode(values):
    out = []
    for v in values:
        if out and out[-1][0] == v:
            out[-1][1] += 1
        else:
            out.append([v, 1])
    return out

# 벡터화 실행: 튜플 하나씩이 아니라 컬럼 배열 전체에 한 번에 연산(sum)을 적용한다.
def vectorized_sum(col):
    return sum(col)  # 실제 엔진은 SIMD 로 배치 처리하지만, 여기선 개념만 시연

country_rle = run_length_encode(column_store["country"])
total_amount = vectorized_sum(column_store["amount"])

print("row store bytes touched for SUM(amount):", row_store_bytes_touched)
print("column store bytes touched for SUM(amount):", column_store_bytes_touched)
print("country column RLE:", country_rle)          # [['KR', 2], ['US', 3]]
print("vectorized SUM(amount):", total_amount)      # 485

연습

같은 이벤트 데이터를 Postgres와 DuckDB에 각각 적재해 대규모 집계 질의의 스캔량과 소요 시간을 비교하고 컬럼 프루닝 효과를 확인하라.

실무 · Verex 연결

체인 이벤트 로그를 인덱싱해 거래량·미결제약정·사용자별 손익을 뽑는 Verex 분석 백엔드는 전형적인 OLAP 워크로드라, 온체인 상태 미러링용 저장소와 분석용 저장소를 분리하는 편이 자연스럽다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 75. 인덱싱 파이프라인 설계77. 스트리밍 처리 의미론 →