Workspace IndexAlgorithms › Day 81

[Review] The Data Model Determines Performance TODO

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

Concept

A data model sets the ceiling on performance before anything else, because it determines physical access paths, not just logical representation. Key design decides which lookups finish in a single seek and which turn into a full scan; the degree of normalization allocates cost between write-time duplication and read-time joins; and partitioning/clustering keys decide which range queries land on adjacent disk blocks. Indexes correct this structure after the fact, but they add write amplification and storage, so you can't pile on indexes indefinitely, and they can't fully rescue a fundamentally wrong model. So the practical order is: first write down which queries need to run at what frequency and latency target, then fit the model to that access pattern — doing it the other way around, fitting queries to a model chosen first, means paying for a migration later. The key point is that the multiplier you get from caching or more hardware is usually a constant factor, while a model change buys you an improvement in complexity order.

Most performance problems come from a schema mismatched to the access pattern, not from code that needs optimizing — and this is also the decision that's most expensive to reverse once data has accumulated.

Code & Formula

# [복습] 데이터 모델이 성능을 정한다 — 같은 데이터라도 접근 경로(키 설계)에 따라
# "단일 탐색"과 "전체 스캔"으로 갈리는 것을 두 가지 인덱스 구조로 비교한다.

orders = [
    {"order_id": i, "user_id": i % 5, "amount": (i * 37) % 200}
    for i in range(1, 21)
]

# 모델 A: order_id 로만 인덱싱 → "user_id=3의 주문 조회"는 전체 스캔이 필요
by_order_id = {o["order_id"]: o for o in orders}

def find_by_user_scan(user_id):
    return [o for o in by_order_id.values() if o["user_id"] == user_id]  # O(N)

# 모델 B: 접근 패턴("user_id로 자주 조회")에 맞춰 미리 파티셔닝/클러스터링
by_user_id = {}
for o in orders:
    by_user_id.setdefault(o["user_id"], []).append(o)  # 쓰기 시 중복 비용을 지불

def find_by_user_indexed(user_id):
    return by_user_id.get(user_id, [])  # O(1) 탐색 + 결과 크기만큼

target_user = 3
scan_result = find_by_user_scan(target_user)
indexed_result = find_by_user_indexed(target_user)

print("query: orders for user_id =", target_user)
print("model A (scan all orders):", [o["order_id"] for o in scan_result])
print("model B (pre-partitioned by user_id):", [o["order_id"] for o in indexed_result])
print("same result:", scan_result == indexed_result)
print("lesson: model B pays write-time cost to buy O(1) reads for the *actual* query pattern")

Exercise

Pull the top three slowest queries in your current service, check their execution plans, and write down alternative key/partition designs that would turn each into a single index lookup, then compare the expected scanned-row counts.

Practical Connection

In Verex, 'all of one user's positions across every market' and 'all of one market's orders across every user' demand different access paths, so making both fast calls for separating storage structures by access pattern rather than trying to force one table design to cover both.

Where it lands in Jayverse

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

Expression뜻 · 쓰이는 자리
set the ceiling on~의 상한선을 결정짓다 · 데이터 모델이 성능의 한계를 미리 정한다는 뜻. "A data model sets the ceiling on performance"
write amplification쓰기 증폭 · 인덱스를 추가할 때 실제 쓰기 작업이 늘어나는 현상. "they add write amplification and storage"
pile on계속 쌓아 올리다, 마구 추가하다 · 인덱스를 무한정 늘릴 수 없다는 뜻. "you can't pile on indexes indefinitely"
rescue잘못된 것을 구제하다, 살려내다 · 인덱스가 근본적으로 틀린 모델을 완전히 구하지는 못한다는 뜻. "they can't fully rescue a fundamentally wrong model"
mismatched to~와 맞지 않는, 어긋나는 · 스키마가 실제 접근 패턴과 안 맞을 때. "a schema mismatched to the access pattern"
expensive to reverse되돌리기에 비용이 많이 드는 · 데이터가 쌓인 뒤 모델을 바꾸기 어렵다는 뜻. "the most expensive to reverse once data has accumulated"

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/.


한국어

[복습] 데이터 모델이 성능을 정한다 TODO

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

개념

데이터 모델은 논리적 표현이 아니라 물리적 접근 경로를 결정하기 때문에 성능의 상한을 먼저 정한다. 키 설계는 어떤 조회가 단일 탐색으로 끝나고 어떤 조회가 전체 스캔이 되는지를 정하고, 정규화 정도는 쓰기 시 중복 비용과 읽기 시 조인 비용 사이의 배분을 정하며, 파티셔닝·클러스터링 키는 어떤 범위 질의가 인접한 디스크 블록에서 처리되는지를 정한다. 인덱스는 이 구조를 사후에 보정하는 수단이지만 쓰기 증폭과 저장 공간을 늘리므로 무한정 늘릴 수 없고, 근본적으로 잘못된 모델을 인덱스로 완전히 구제하지는 못한다. 그래서 실무 순서는 '어떤 질의를 어떤 빈도와 지연 목표로 처리할 것인가'를 먼저 적고 그 접근 패턴에 맞춰 모델을 정하는 것이며, 반대로 모델을 먼저 정하고 질의를 끼워 맞추면 나중에 마이그레이션 비용을 치르게 된다. 캐시나 하드웨어 증설로 얻는 배수는 대개 상수배지만 모델 변경으로 얻는 것은 복잡도 차수의 개선이라는 점이 핵심이다.

성능 문제 대부분은 코드 최적화가 아니라 접근 패턴과 어긋난 스키마에서 오고, 이 결정은 데이터가 쌓인 뒤에 되돌리기가 가장 비싼 결정이기도 하다.

코드 · 수식

# [복습] 데이터 모델이 성능을 정한다 — 같은 데이터라도 접근 경로(키 설계)에 따라
# "단일 탐색"과 "전체 스캔"으로 갈리는 것을 두 가지 인덱스 구조로 비교한다.

orders = [
    {"order_id": i, "user_id": i % 5, "amount": (i * 37) % 200}
    for i in range(1, 21)
]

# 모델 A: order_id 로만 인덱싱 → "user_id=3의 주문 조회"는 전체 스캔이 필요
by_order_id = {o["order_id"]: o for o in orders}

def find_by_user_scan(user_id):
    return [o for o in by_order_id.values() if o["user_id"] == user_id]  # O(N)

# 모델 B: 접근 패턴("user_id로 자주 조회")에 맞춰 미리 파티셔닝/클러스터링
by_user_id = {}
for o in orders:
    by_user_id.setdefault(o["user_id"], []).append(o)  # 쓰기 시 중복 비용을 지불

def find_by_user_indexed(user_id):
    return by_user_id.get(user_id, [])  # O(1) 탐색 + 결과 크기만큼

target_user = 3
scan_result = find_by_user_scan(target_user)
indexed_result = find_by_user_indexed(target_user)

print("query: orders for user_id =", target_user)
print("model A (scan all orders):", [o["order_id"] for o in scan_result])
print("model B (pre-partitioned by user_id):", [o["order_id"] for o in indexed_result])
print("same result:", scan_result == indexed_result)
print("lesson: model B pays write-time cost to buy O(1) reads for the *actual* query pattern")

연습

현재 서비스에서 가장 느린 상위 세 질의를 뽑아 실행 계획을 확인하고, 각각을 단일 인덱스 탐색으로 만들 수 있는 키·파티션 설계를 대안으로 적어 예상 스캔 행 수를 비교해 보라.

실무 · Verex 연결

Verex에서 '한 사용자의 전 마켓 포지션'과 '한 마켓의 전 사용자 주문'은 서로 다른 접근 경로를 요구하므로, 두 질의를 모두 빠르게 하려면 하나의 테이블 설계로 버티기보다 접근 패턴별 저장 구조를 나누는 판단이 필요하다.

Jayverse에서의 위치

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

Expression뜻 · 쓰이는 자리
set the ceiling on~의 상한선을 결정짓다 · 데이터 모델이 성능의 한계를 미리 정한다는 뜻. "A data model sets the ceiling on performance"
write amplification쓰기 증폭 · 인덱스를 추가할 때 실제 쓰기 작업이 늘어나는 현상. "they add write amplification and storage"
pile on계속 쌓아 올리다, 마구 추가하다 · 인덱스를 무한정 늘릴 수 없다는 뜻. "you can't pile on indexes indefinitely"
rescue잘못된 것을 구제하다, 살려내다 · 인덱스가 근본적으로 틀린 모델을 완전히 구하지는 못한다는 뜻. "they can't fully rescue a fundamentally wrong model"
mismatched to~와 맞지 않는, 어긋나는 · 스키마가 실제 접근 패턴과 안 맞을 때. "a schema mismatched to the access pattern"
expensive to reverse되돌리기에 비용이 많이 드는 · 데이터가 쌓인 뒤 모델을 바꾸기 어렵다는 뜻. "the most expensive to reverse once data has accumulated"

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

← 1133. 외부 정렬·병합 전략과 병렬 정렬 (TAOCP 3권)1135. 랜덤 오라클·길이 연장 공격·도메인 분리 →