"대규모 시스템 설계 면접" 책을 보다 보면 메트릭 모니터링이나 광고 클릭 집계 챕터에서 컬럼형 DB(컬럼 지향 데이터베이스)가 해법으로 툭툭 나온다. 근데 "왜 하필 여기서 컬럼형이지?"가 잘 와닿지 않았다. 평소에 MySQL만 쓰다 보니 더 그랬던 것 같다.
그래서 한 번 제대로 정리해보기로 했다. 글이 길어질 것 같아서 두 편으로 나눴는데, 이번 편은 이론이다. 컬럼형 DB가 뭔지, MySQL 같은 row DB와 내부적으로 뭐가 다른지, 어떤 워크로드에서 빨라지는지를 정리한다. 다음 편에서는 DB를 직접 띄워서 같은 데이터로 MySQL과 성능을 비교해볼 생각이다.
컬럼형 DB란 - 디스크에 데이터를 늘어놓는 방식의 차이
컬럼형 DB를 한 줄로 설명하면 이렇다.
같은 테이블 데이터를, 디스크에 컬럼 단위로 묶어서 저장하는 DB.
아래 테이블을 디스크에 저장한다고 해보자.
| id | name | age | city |
|---|---|---|---|
| 1 | 철수 | 25 | 서울 |
| 2 | 영희 | 30 | 부산 |
| 3 | 민수 | 28 | 서울 |
MySQL 같은 row-oriented DB는 한 행을 통째로 묶어서 디스크에 연속으로 저장하고, column-oriented DB는 같은 데이터를 컬럼끼리 묶어서 저장한다.

같은 논리 테이블이고, 차이는 "디스크에 물리적으로 어떻게 줄 세우느냐" 뿐이다. 처음 봤을 땐 이게 뭐 별거 있나 싶었는데, 정리하다 보니 이 "디스크에 연속으로 저장한다" 는 한 줄이 사실상 컬럼형 DB의 모든 것이었다. 장점도 단점도 전부 여기서 나온다.
왜 빠른가 - 1. 필요한 컬럼만 읽는다
예시 쿼리를 하나 보자.
SELECT AVG(age) FROM users;
이 쿼리에서 실제로 필요한 컬럼은 age 하나뿐이다. row-oriented는 age만 필요한데도 id, name, city까지 통째로 끌려온다. row 단위로 묶여 있어서 row의 일부만 골라 읽는 게 불가능하기 때문이다. 반면 column-oriented는 age 컬럼 청크 위치로 점프해서 거기만 읽으면 된다.

| Row-oriented | Column-oriented | |
|---|---|---|
AVG(age) 시 읽는 데이터량 |
전체 테이블 | age 컬럼만 |
| 디스크 I/O | 큼 | 작음 |
| 유리한 쿼리 패턴 | 행 전체를 가져오는 쿼리 (SELECT *) |
일부 컬럼만 집계/스캔 |
쿼리에서 읽는 컬럼 비율이 작을수록 컬럼형이 유리하다는 얘기다. 반대로 SELECT *처럼 전체 컬럼을 다 읽는 쿼리라면 컬럼형의 I/O 이점은 사라진다. 오히려 컬럼별로 흩어진 값을 다시 row로 재조립하는 비용 때문에 row DB가 더 빠를 수도 있다. 이건 좀 의외였다.
왜 빠른가 - 2. 압축이 훨씬 잘 된다
컬럼형의 또 다른 강점은 압축이다. 컬럼 단위로 모아두면 압축이 잘 되는 이유는 크게 두 가지다.
하나는 같은 컬럼은 같은 타입이라는 것. age면 전부 정수, city면 전부 문자열이다. 타입이 같은 데이터끼리 모여 있으면 압축 알고리즘이 일관된 패턴을 잡기 쉽다. row-oriented는 한 row 안에 정수·문자열이 섞여 있어서 압축률이 떨어진다.
다른 하나는 같은 컬럼은 값이 비슷하거나 반복되는 경우가 많다는 것. country 컬럼이라면 "Korea, Korea, Korea, USA, ..." 처럼 같은 값이 줄지어 나온다. 예를 들어 정렬된 city 컬럼이라면 아래처럼 압축할 수 있다 (Run-Length Encoding).

row-oriented에서는 city 옆에 항상 age, name 같은 다른 타입 값이 끼어 있어서 "서울이 5번 연속" 같은 패턴 자체가 디스크에 만들어지지 않는다.
압축이 잘 되면 저장 공간만 아끼는 게 아니다. 같은 데이터를 더 적은 바이트로 읽으면 되니까, ①의 I/O 이점이 한 번 더 곱해진다. 컬럼형이 분석 쿼리에서 빠른 진짜 이유는 결국 이 ①과 ②의 조합인 것 같다.
Row DB와의 비교 - OLTP vs OLAP
두 DB는 애초에 다른 목적으로 만들어졌다. 이걸 OLTP(온라인 트랜잭션 처리) / OLAP(온라인 분석 처리) 라고 부른다.
| OLTP (Transaction) | OLAP (Analytical) | |
|---|---|---|
| 대표 작업 | 주문 생성, 회원 정보 수정, 단건 조회 | 매출 집계, 통계, 대시보드 분석 |
| 읽기 패턴 | 특정 row 몇 개를 핀포인트 | 수백만 row를 훑어 집계 |
| 쓰기 패턴 | 단건이 매우 빈번 | 대량을 한 번에 적재 |
| 어울리는 DB | Row DB (MySQL) | 컬럼형 DB |
MySQL은 OLTP, 컬럼형은 OLAP 쪽이다. 그리고 이 차이를 만드는 게 결국 저장 방식이다.
페이지(Page) - row DB가 "다 읽어야 하는" 이유
InnoDB는 디스크에서 데이터를 row 하나씩 읽지 않는다. 페이지라는 고정 크기 블록(기본 16KB) 단위로 읽고 쓴다. WHERE id = 5로 row 하나만 필요해도, 그 row가 들어 있는 16KB 페이지 전체를 통째로 버퍼 풀로 올린다. 디스크는 작은 걸 찔끔찔끔 읽는 것보다 어느 정도 큰 덩어리를 한 번에 읽는 게 효율적이라 이렇게 설계됐다.
그리고 그 16KB 페이지 안에는 여러 row가 모든 컬럼을 포함한 채로 담겨 있다.

그러니 AVG(age)를 구하려고 age만 필요해도, age는 각 row 안에 끼어 있고 그 row들은 페이지에 통째로 담겨 있으니, name·city까지 들어 있는 페이지를 결국 다 읽게 된다. 앞에서 "row DB는 다 읽어야 한다"고 했던 게 물리적으로는 이런 이유이다.
기능/메커니즘 비교
| 항목 | Row DB (MySQL) | 컬럼형 DB |
|---|---|---|
| 단건 INSERT/UPDATE | 빠름 (한 군데에 기록) | 느림 (컬럼마다 흩뿌려 기록) |
| 대량 데이터 적재 | 보통 | 매우 강함 |
| 특정 row 1건 조회 | 빠름 (B-Tree로 핀포인트) | 느림 |
| 대량 집계 스캔 | 느림 | 빠름 |
| 인덱스 전략 | B-Tree로 특정 row를 찾음 | 풀스캔 + 압축 + 블록 스킵 |
| 트랜잭션(ACID) | 강함 | 약하거나 제한적 |
컬럼형의 최대 약점 - 단건 쓰기
새 row [4, 지수, 22, 인천] 하나를 추가한다고 해보자. row DB는 페이지 하나에 통째로 한 번만 기록하면 끝이다. 그런데 컬럼형은 값들이 컬럼마다 흩어져 있으니 id, name, age, city 네 곳에 각각 따로 기록해야 한다. 게다가 컬럼 데이터는 보통 압축된 채 저장돼 있어서, 단건을 끼워 넣으려면 압축 블록을 풀고 → 넣고 → 다시 압축해야 한다. 한 건 넣자고 이걸 매번 한다고 생각하면 막막하다.
그래서 컬럼형 DB는 단건 쓰기를 싫어하고, 대신 수만~수백만 건을 모아 한 번에 적재하는 방식으로 이 비용을 분산시킨다.
여기서 들었던 궁금중이 있다. "한 건씩 append하는 것도 결국 단건 쓰기랑 같은 것 아닌가?" 컬럼형이 싫어하는 건 단순히 "한 건"이 아니라 이미 디스크에 압축·정렬된 채 누워 있는 블록의 중간을 건드리는 것이었다. append는 맨 뒤에 덧붙이는 거라 기존 블록을 안 건드린다. 게다가 컬럼형/시계열 DB는 들어오는 append를 한 건씩 곧바로 디스크에 쓰지 않는다. 메모리 버퍼에 모았다가 충분히 차면 컬럼별로 정렬·압축해서 한 번에 flush 한다. 논리적으로는 단건 append처럼 보여도 실제 디스크 쓰기는 대량 배치인 셈이다. 그래서 append와 대량 적재에 강하다.
다만 오해하면 안 되는 게, "모아서 쓰기" 자체는 row DB도 한다(버퍼 풀, 그룹 커밋 등). 차이는 그 배치가 만들어내는 최종 디스크 레이아웃이다. 같은 배치 쓰기라도 row DB는 "row들이 통째로 담긴 페이지"로 떨어지고, 컬럼형은 "컬럼별 압축 블록"으로 떨어진다. 그리고 그 레이아웃이 이후의 분석 읽기 성능을 가른다. 그러니까 컬럼형의 승부처는 쓰기 자체가 아니라 쓴 다음의 집계 읽기다.
저장 단위와 정렬, 인덱스
Row Group → Column Chunk → Page
컬럼형이라고 컬럼을 파일 전체에 걸쳐 통짜로 저장하는 건 아니다. 먼저 행을 큰 덩어리로 자르고, 그 덩어리 안에서만 컬럼 지향으로 저장한다 (Parquet, ORC 같은 대표 포맷 기준).

여기서 Page가 압축의 최소 단위이고, row DB의 16KB 페이지에 대응하는 개념이다. Row Group은 스킵의 단위다.
존 맵(Zone Map) - 안 봐도 되는 덩어리를 통째로 버린다
각 Row Group마다 그 안의 min/max 값을 메타데이터로 적어둔다. 이걸 존 맵이라고 한다. 시간 범위 쿼리가 들어오면, 범위에서 벗어난 Row Group은 통째로 건너뛴다.

컬럼형은 B-Tree로 row를 핀포인트하는 대신, 이렇게 안 봐도 되는 블록을 통째로 버리는 식으로 읽을 양을 줄인다.
정렬이 핵심이다
근데 이 스킵은 데이터가 정렬돼 있을 때만 잘 먹힌다. 정렬이 안 된 채 무작위로 섞여 들어가면 모든 Row Group이 거의 전 범위를 품게 되어서, 하나도 못 버리고 사실상 풀스캔이 된다. 정렬이 영향을 주는 건 앞에서 본 세 이점 중 "블록 스킵" 하나뿐이라는 것도 알아두면 좋다.
| 이점 | 정렬 필요? |
|---|---|
| 필요한 컬럼만 읽기 | 정렬 무관, 항상 유효 |
| 압축 | 정렬되면 좋아지지만 안 돼도 동작 |
| 존 맵 블록 스킵 | 정렬에 결정적으로 의존 |
여기서 시계열 데이터의 궁합이 좋아진다. 시계열은 자연스럽게 시간순으로 들어오니 timestamp 정렬이 거의 공짜로 되고, 그래서 시간 범위 쿼리의 스킵이 잘 먹힌다.
참고로 정렬 여부는 DB가 런타임에 검사하는 게 아니다. ORDER BY (timestamp) 처럼 정렬 키를 선언하면 엔진이 적재할 때 그 키로 정렬해서 저장하니까, 정렬은 추측이 아니라 보장된 사실이다.
컬럼형의 인덱스 - sparse 인덱스
컬럼형에도 인덱스가 있는데 성격이 다르다. row DB의 B-Tree는 거의 모든 값을 가리키는 조밀한(dense) 인덱스라 정확한 row 위치를 알려준다. 컬럼형은 정렬된 데이터를 일정 블록(예: 8192 row)으로 나누고 각 블록의 첫 값만 기록하는 희소한(sparse) 인덱스를 쓴다.

WHERE timestamp = 09:20을 찾으면, 인덱스에서 09:15 ≤ 09:20 < 09:30이니 블록1만 읽고 그 안을 스캔한다. "정확한 위치"가 아니라 "어느 블록인지"까지만 좁혀주는 셈이다. 컬럼형은 어차피 블록 단위로 읽으니 이걸로 충분하고, 인덱스가 수천 배 작아서 메모리에 통째로 올려둘 수 있다. 단, 이것도 정렬 키에 대해서만 동작한다.
그럼 정렬 키가 아닌 컬럼으로도 빠르게 조회하려면? 한 저장본은 단일 정렬만 가질 수 있어서(컬럼별로 따로 정렬하면 row 재조립이 깨진다) 컬럼별 독립 정렬은 안 된다. 대신 같은 데이터를 다른 정렬 순서로 여러 벌 복제 저장하는 projection 같은 방법을 쓴다. row DB가 세컨더리 인덱스를 여러 개 두는 자리에, 컬럼형은 정렬 복제본을 둔다고 보면 될 것 같다. 압축률이 높으니 복제본을 더 둬도 부담이 상대적으로 덜하다.
시스템 설계 사례로 보기
강점/약점을 머리에 넣고 보면, 어떤 케이스에서 컬럼형 DB가 강점을 보이는지를 쉽게 이해할 수 있다.
사례 1 - 메트릭 모니터링 & 알림 시스템
서버 수천 대의 CPU·메모리·QPS 같은 지표를 초 단위로 수집해 저장하고, 대시보드로 보거나 임계치 알림을 쏘는 시스템이다. 데이터는 (timestamp, metric, host, value) 형태의 시계열이다.
특징이 두 가지다. 쓰기는 초당 수백만 건이 계속 append되기만 하고 과거 단건 수정은 거의 없다. 읽기는 "지난 1시간 web-01의 cpu_usage 평균을 1분 단위로" 같은 특정 컬럼 + 시간 범위 집계가 대부분이다. 이 프로파일이 컬럼형과 거의 그대로 맞아떨어진다.
| 컬럼형의 특성 | 메트릭 모니터링에서 | 결과 |
|---|---|---|
| 단건 쓰기 약함 | 단건 쓰기가 없음 (append만) | 약점 회피 |
| 대량 적재 강함 | 초당 수백만 건 적재 | 강점 활용 |
| 필요 컬럼만 읽기 | value만 집계 | I/O 최소 |
| 압축 강함 | 시계열은 압축 천국 | 저장·I/O 절감 |
특히 시계열은 압축이 잘 먹힌다. timestamp는 일정 간격으로 증가하니 "시작값 + 간격"으로(델타 인코딩), value는 비슷한 숫자가 연속이라 잘 압축되고, host·metric은 같은 값이 반복돼 RLE로 거의 0에 수렴한다. InfluxDB, Prometheus 같은 시계열 DB가 내부적으로 컬럼형 저장 구조를 쓰는 게 이런 이유인 것 같다.
사례 2 - 광고 클릭 이벤트 집계
사용자가 광고를 클릭할 때마다 (ad_id, click_time, user_id, country, ...) 이벤트가 발생한다. 대형 광고 플랫폼이면 일 수십억 건이다. 이걸로 광고주 대시보드, 과금, 부정 클릭 탐지를 한다. 핵심 쿼리는 "지난 1분간 ad_id=X의 클릭 수", "지난 1시간 top 100 광고" 같은 시간 윈도우 집계다.
데이터 성격은 사례 1과 거의 판박이다. append-only 대량 이벤트, 특정 컬럼 집계, 시간축 정렬, 반복 많은 컬럼. 그래서 앞서 본 메커니즘이 그대로 적용된다. 다만 여기엔 새 포인트가 하나 더 있다.
사전 집계(pre-aggregation) 다. 일 수십억 건의 raw 클릭을 그대로 두고 매번 집계하면, 대시보드 쿼리 한 번에 수십억 row를 스캔해야 한다. 그래서 들어오는 단계에서 스트리밍 처리(Kafka + Flink 등)로 분 단위로 미리 묶어 데이터 양 자체를 줄인다.

여기서 컬럼형은 두 역할을 한다. 사전 집계 결과 테이블도 여전히 큰 데다 그 위에 시간 롤업·국가별 재집계 같은 2차 집계가 계속 들어오니 이걸 빠르게 처리하고, 동시에 부정 클릭 재집계를 위해 raw 데이터도 보관하는 저장소가 된다.
하나 더. 컬럼형은 "필요 컬럼만 읽기"에서 한 발 더 나가, 읽은 다음 CPU에서 집계할 때도 빠르다. 한 컬럼의 값들이 메모리에 연속으로 모여 있어서 SUM/COUNT를 벡터화(SIMD)로 처리하고 CPU 캐시 효율도 좋기 때문이다. row DB는 값들이 다른 컬럼과 섞여 흩어져 있어서 이 최적화가 어렵다.
정리, 그리고 다음 편
두 사례 다 따지고 보면 "대량으로 쌓고(append) → 시간/컬럼 기준으로 집계해 읽는다" 는 같은 패턴이다. 이게 컬럼형의 강점과 맞아떨어지니까 자꾸 해법으로 등장하는 거였다. 처음엔 "컬럼으로 저장한다"가 별거 아닌 차이로 보였는데, 필요 컬럼만 읽기, 압축, 블록 스킵, 단건 쓰기 약점, 사전 집계와의 궁합까지 전부 이 한 줄에서 나온다는 게 정리하고 나서야 눈에 들어왔다.
반대로 단건 조회·수정이 빈번한 OLTP라면 여전히 MySQL 같은 row DB가 맞다. 결국 워크로드 따라 고르는 문제인 것 같다.
여기까지가 이론편이다. 근데 글로만 정리하니까 "진짜 그만큼 차이가 나나?" 싶은 게 솔직히 안 가신다,, 그래서 다음 편에서는 컬럼형 DB를 직접 띄워서, 같은 데이터·같은 쿼리로 MySQL과 성능을 비교해볼 생각이다. 이론에서 말한 "필요 컬럼만 읽기"나 "압축"이 실제 쿼리 시간으로 얼마나 드러나는지 숫자로 확인해보려고 한다.
'개발 공부 > DB' 카테고리의 다른 글
| 왜 서로 다른 값을 INSERT 했는데 데드락이 걸릴까? (0) | 2026.02.02 |
|---|