[Review] The Data Model Determines Performance TODO
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")
docs/code/algorithms/algorithms-81.py
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/.