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.

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

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

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