Skip to content
Published on

데이터베이스 내부 구조 완전 해부 — B-Tree, LSM, WAL, MVCC, Vacuum, Index, Query Planner, Replication 심층 가이드 (2025)

Authors

왜 DB 내부를 알아야 하는가 — 2025년의 관점

"나는 SELECT * FROM users WHERE email = ?만 쓰면 되는데 내부까지 알아야 해?"

예, 알아야 합니다. 이유:

  1. AI 기반 앱 폭증 — pgvector, Qdrant, LanceDB 같은 벡터 DB는 HNSW 같은 인덱스 구조의 특성이 직접 비용과 품질에 드러난다.
  2. 클라우드 비용 압박 — 쿼리 플랜 하나 잘못되면 RDS 인스턴스를 2배로 키워야 한다. 이해 없이는 튜닝이 불가능하다.
  3. DB가 '초당 수백만' 처리 가능한 시대 — PostgreSQL 17, MySQL 8.4, DuckDB 1.x가 내부 구조를 끊임없이 개선하는 이유를 알아야 올바르게 쓴다.
  4. LLM이 SQL은 생성하지만, 플랜은 못 만든다 — 사람의 영역이 내부 이해로 옮겨왔다.
  5. 분산 DB가 보편화 — 직전 글에서 본 CockroachDB·TiDB·YugabyteDB의 성능 특성은 하부 엔진의 선택(RocksDB vs Pebble)에서 결정된다.

Part 1 — 모든 DB가 공유하는 한 가지: WAL

Write-Ahead Log의 본질

"데이터 파일을 변경하기 전에, 변경 내역을 먼저 로그에 기록한다."

왜? 크래시 리커버리 때문이다. 데이터 페이지 변경 도중 전원이 나가면, 파일은 반쯤 쓰인 상태가 된다. WAL이 있으면 "어디까지 했고, 뭐가 남았는지" 재구성할 수 있다.

1. 트랜잭션 시작
2. BEGINWAL에 기록
3. UPDATE users SET name='A' WHERE id=1
WAL"변경 전/후 값" 기록
   → 데이터 페이지는 아직 디스크에 안  (메모리만)
4. COMMIT
WALCOMMIT 기록
   → fsync로 디스크에 강제 쓰기
5. (나중에) 데이터 페이지 플러시 (체크포인트)

ARIES 알고리즘 (1992, IBM)

현대 대부분의 DBMS가 ARIES(Algorithms for Recovery and Isolation Exploiting Semantics)를 따른다.

3단계 복구:

  1. Analysis — WAL을 읽어 "크래시 시점에 살아있던 트랜잭션" 식별.
  2. Redo — WAL의 모든 변경을 재적용 (커밋 안 된 것 포함).
  3. Undo — 커밋되지 않은 트랜잭션을 되돌림.

PostgreSQL, Oracle, SQL Server, MySQL InnoDB 모두 ARIES 계열. 세부 구현은 다르지만 본질은 같다.

WAL의 성능 이슈

  • Group Commit — 여러 트랜잭션의 WAL을 묶어 한 번에 fsync. 처리량 10-100배.
  • Parallel Apply — 복제본에서 WAL을 병렬 적용 (PostgreSQL 16+).
  • WAL Compression — 페이지 이미지를 압축 (pglz/zstd/lz4).
  • Logical Replication — 바이너리 WAL이 아닌 "논리적 변경 이벤트"로 변환.

Part 2 — 저장 엔진의 두 거대 계파: B-Tree vs LSM-Tree

B-Tree (Bayer & McCreight, 1971)

대부분의 전통 RDBMS(PostgreSQL, MySQL InnoDB, Oracle)의 기본. 실제로는 B+Tree — 리프 노드끼리 연결리스트로 이어져 순차 스캔 가능.

        [20 | 50 | 80]        (내부 노드)
       /    |    |    \
  [1,10][25,40][55,70][85,95]  (리프, 실제 데이터 or 포인터)

특성:

  • 읽기 비용: O(log n) — 트리 높이 3-4면 수십억 행.
  • 쓰기 비용: O(log n) — 단, 페이지 분할이 일어나면 연쇄 쓰기.
  • 범위 쿼리 강력 — 리프 순차 탐색.

단점: 쓰기 증폭(Write Amplification) — 한 번의 논리 쓰기가 여러 페이지 디스크 쓰기를 유발.

LSM-Tree (O'Neil et al., 1996)

Log-Structured Merge-Tree. Google Bigtable(2006)이 대중화. RocksDB, Cassandra, HBase, LevelDB, ScyllaDB, InfluxDB가 모두 LSM.

구조:

MemTable (메모리, 정렬된 구조: skip list or red-black tree)
    (꽉 차면)
L0 파일 (SSTable, 정렬된 불변 파일)
    (컴팩션)
L1 파일들
L2 파일들
...

쓰기 경로:

  1. WAL에 기록
  2. MemTable에 삽입 (메모리)
  3. MemTable 가득 → L0로 플러시
  4. 백그라운드에서 컴팩션(여러 L0 → L1 통합, 중복 제거)

읽기 경로:

  1. MemTable 확인
  2. L0 파일들 확인 (최신 순)
  3. L1, L2, ... 확인
  4. Bloom Filter로 "이 SSTable에 해당 키 없음"을 빠르게 판정.

장점:

  • 쓰기 처리량 극대화 — 랜덤 쓰기가 순차 쓰기로 변환.
  • 압축 효율 높음 — 정렬된 상태로 저장.

단점:

  • 읽기 증폭(Read Amplification) — 여러 SSTable 확인 필요.
  • 공간 증폭(Space Amplification) — 컴팩션 전까진 중복 데이터 존재.
  • 컴팩션이 I/O/CPU 소모.

비교 — 언제 B-Tree, 언제 LSM?

상황추천
OLTP (읽기 중심, 범위 쿼리)B-Tree
쓰기 집중 (로그, IoT, 시계열)LSM
SSD 수명 고려LSM (쓰기 증폭 낮음)
복잡한 트랜잭션B-Tree (MVCC 구현 쉬움)
키-밸류만LSM (RocksDB)

흥미로운 2020년대 트렌드: 두 계파의 융합.

  • CockroachDB는 처음에 RocksDB를 썼다가, 2021년 자체 Go LSM 엔진 Pebble로 교체.
  • PostgreSQL의 Heap + B-Tree 인덱스 조합은 LSM에 없는 강점.
  • **Aurora DSQL(AWS 2024)**은 전통 PostgreSQL 스토리지와 분산 WAL을 결합.

Part 3 — MVCC — 읽는 사람이 쓰는 사람을 막지 않는 마법

MVCC의 핵심 아이디어

"하나의 행에 여러 버전을 유지해, 각 트랜잭션이 자기 시점의 스냅샷을 본다."

전통적 잠금 기반 DB는 읽기가 쓰기를 블로킹한다. MVCC는 그걸 없앤다.

PostgreSQL의 MVCC 구현

모든 행에 숨겨진 컬럼 4개가 있다:

-- 내부적으로 모든 행은:
SELECT xmin, xmax, cmin, cmax, ctid, *
FROM users;
  • xmin: 이 행을 생성한 트랜잭션 ID.
  • xmax: 이 행을 삭제/업데이트한 트랜잭션 ID (0이면 유효).
  • cmin, cmax: 같은 트랜잭션 내 명령 순서.
  • ctid: 물리적 위치 (페이지, 오프셋).

가시성 규칙:

행이 보이려면:
  xmin이 커밋된 트랜잭션이고,
  xmin <= 현재_트랜잭션_스냅샷,
  그리고 (xmax == 0 or xmax가 롤백됨 or xmax > 현재_스냅샷)

UPDATE는 실제로는 DELETE + INSERT. 기존 행의 xmax를 설정하고, 새 행을 삽입한다. 그래서 PostgreSQL의 UPDATE는 느리다고 알려져 있다.

Oracle/MySQL InnoDB 방식 — Undo Log

PostgreSQL처럼 행 안에 버전을 두는 대신, 별도의 Undo 영역에 과거 버전을 기록.

데이터 페이지: 최신 값만
Undo Log: (과거 값 체인)

장점: Vacuum이 필요 없다 (Undo는 자동 정리). 단점: Undo 체인이 길어지면 읽기 비용 증가. "ORA-01555 snapshot too old" 유명 에러.

Vacuum의 진실 — "느려지는 이유"

PostgreSQL MVCC의 대가는 Dead Tuple이다. 업데이트/삭제된 행이 디스크에 남는다. VACUUM이 이를 정리한다.

Autovacuum이 못 따라가면:

  • 테이블 블로트(bloat) — 실제 데이터의 2-5배 공간 차지.
  • 인덱스 블로트 — 더 심각함.
  • 쿼리 플랜 왜곡 — 통계가 부정확.

해결책:

  • autovacuum_vacuum_scale_factor 낮추기 (기본 0.2 → 테이블의 20%가 dead가 돼야 작동).
  • 큰 테이블은 autovacuum_vacuum_insert_scale_factor (PG 13+).
  • pg_repack으로 온라인 테이블 재구축.
  • PostgreSQL 16부터 parallel vacuum 강화.

Serializable Snapshot Isolation (SSI)

PostgreSQL 9.1+. 진정한 serializable을 MVCC 기반으로 구현. 전통적 S2PL보다 동시성 훨씬 높음. CockroachDB도 유사 알고리즘.

Part 4 — 인덱스의 동물원

B-Tree Index

PostgreSQL 기본. 대부분의 경우 최적.

CREATE INDEX idx_users_email ON users(email);

유용한 경우: 동등(=), 범위(<, >, BETWEEN), ORDER BY, LIKE 'prefix%'.

Hash Index

동등 비교만. 2020년대엔 B-Tree가 거의 모든 경우 더 좋아 잘 안 쓴다.

BRIN (Block Range INdex)

큰 테이블의 "거의 정렬된" 컬럼에 이상적. 테이블 전체를 블록 단위로 요약만 한다.

CREATE INDEX idx_logs_timestamp ON logs USING BRIN(timestamp);
  • 인덱스 크기: B-Tree의 0.1%.
  • 시계열 데이터에 완벽.
  • 순서가 흐트러지면 효율 급락.

GIN (Generalized Inverted iNdex)

배열, JSONB, 전문 검색에 쓰는 역색인.

CREATE INDEX idx_users_tags ON users USING GIN(tags);
CREATE INDEX idx_events_data ON events USING GIN(data jsonb_path_ops);
CREATE INDEX idx_docs_text ON docs USING GIN(to_tsvector('english', body));
  • 쓰기 느림(업데이트 시 많은 포스팅 리스트 갱신).
  • 읽기 매우 빠름.
  • fastupdate + pending list로 쓰기 비용 분산.

GiST (Generalized Search Tree)

기하학, 범위 타입, 전문 검색에 쓰는 확장 가능 트리.

CREATE INDEX idx_locations ON stores USING GIST(location);  -- PostGIS
CREATE INDEX idx_booking ON bookings USING GIST(duration);  -- tsrange

SP-GiST

공간 분할(Space-Partitioned) GiST. IP 주소, 전화번호처럼 불균형 데이터에 강함.

HNSW — 벡터 검색의 지배자

Hierarchical Navigable Small World. pgvector 0.5+에서 도입, 2024년 벡터 검색의 사실상 표준.

CREATE EXTENSION vector;
CREATE TABLE documents(id BIGSERIAL, embedding vector(1536));

CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

원리:

  • 여러 레이어의 그래프 — 위층은 긴 거리 점프, 아래층은 세밀한 탐색.
  • "소셜 네트워크의 6단계 분리"와 같은 small-world 성질 이용.
  • 쿼리: 위에서 시작 → 가까운 이웃으로 내려감 → 최하층에서 정답 후보 반환.

경쟁자: IVFFlat — 더 간단하지만 HNSW가 정밀도·속도 모두 앞선다.

2024 벤치마크: pgvector 0.7 + HNSW는 Pinecone과 근접 성능, 비용은 1/10.

DiskANN

Microsoft Research의 SSD 친화 그래프 인덱스. 수십억 벡터를 SSD에서 검색 가능. pgvector 0.8+에서 실험적 지원.

Part 5 — Query Planner가 하는 일

4단계

  1. Parsing — SQL을 파싱 트리로.
  2. Rewriting — 뷰 확장, 룰 적용.
  3. Planning — 여러 실행 계획을 생성·비교.
  4. Execution — 선택된 계획 실행.

Cost-Based Optimization (CBO)

"몇 가지 계획을 만들어 각 비용을 추정, 최저 비용 선택."

비용 = seq_page_cost * 페이지_수 + random_page_cost * 랜덤_페이지_수 + cpu_tuple_cost * 행수 + ...

기본값:

  • seq_page_cost = 1.0
  • random_page_cost = 4.0 (HDD 기준) — SSD면 1.1로 내려야!
  • cpu_tuple_cost = 0.01

통계 (pg_statistic)

플래너는 테이블 통계 없이는 무력하다. ANALYZE가 수집하는 정보:

  • n_distinct: 컬럼의 고유값 수.
  • most_common_vals / most_common_freqs: 자주 나오는 값.
  • histogram_bounds: 범위 쿼리 선택성 추정.
  • correlation: 물리적 정렬 vs 논리적 정렬의 상관관계.

통계가 오래되면 플래너가 바보가 된다. autovacuum이 ANALYZE도 하므로, autovacuum 죽이면 안 된다.

Join 전략

  1. Nested Loop — 작은 테이블 + 인덱스 있는 큰 테이블.
  2. Hash Join — 한쪽이 메모리에 들어올 때.
  3. Merge Join — 양쪽이 정렬돼 있을 때.
EXPLAIN ANALYZE SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at > '2025-01-01';

이 결과를 읽을 줄 아는 것이 DB 엔지니어의 기본.

Parameter Sniffing / Plan Cache 문제

준비된 문장(prepared statement)은 첫 실행 시 플랜을 캐시한다. 이후 다른 파라미터로 실행하면 부적절한 플랜이 재사용되어 느려질 수 있다. PostgreSQL은 plan_cache_mode 설정으로 제어.

PostgreSQL JIT

PG 11+. 복잡한 WHERE 절·집계를 LLVM으로 JIT 컴파일. OLAP 쿼리에 2-10배 속도. 단, 짧은 OLTP 쿼리엔 오히려 오버헤드 — jit_above_cost 임계 설정이 중요.

Part 6 — Replication의 구조

1. Streaming Replication (물리 복제)

PostgreSQL의 기본. WAL을 네트워크로 그대로 전송, 복제본이 재생.

PrimaryWAL StreamReplica
  • 바이트 단위 정확한 복사 — 모든 DDL/DML 포함.
  • 버전/플랫폼 동일 해야 함.
  • 동기/비동기/quorum 모드.
  • Hot Standby — 복제본에서 읽기 쿼리 가능.

2. Logical Replication (논리 복제)

WAL을 "논리적 변경(INSERT user 5, UPDATE row ...)"으로 해석해 전송.

CREATE PUBLICATION my_pub FOR ALL TABLES;
CREATE SUBSCRIPTION my_sub CONNECTION '...' PUBLICATION my_pub;
  • 서로 다른 PG 버전 간 복제 가능.
  • 테이블 단위 선택적 복제.
  • 업그레이드/마이그레이션에 활용.
  • Debezium이 이를 Kafka로 전달.

3. Trigger-Based Replication

레거시. pg_logical 이전 시대의 Slony 등. 지금은 거의 안 씀.

Async vs Sync 트레이드오프

  • 비동기(기본): Primary 커밋 후 복제본 전송 → 데이터 손실 가능, 지연 낮음.
  • 동기: 복제본이 WAL 수신 확인 후 커밋 → 손실 없음, 지연 증가.
  • Quorum Commit (PG 10+): N개 중 M개 확인 — 균형.

복제 지연 모니터링

SELECT
  application_name,
  pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag,
  pg_wal_lsn_diff(sent_lsn, flush_lsn) AS flush_lag,
  pg_wal_lsn_diff(flush_lsn, replay_lsn) AS replay_lag
FROM pg_stat_replication;

Part 7 — 파티셔닝과 샤딩

파티셔닝 (단일 DB 내)

PostgreSQL 10+의 네이티브 파티셔닝:

CREATE TABLE events (
  id BIGSERIAL,
  created_at TIMESTAMPTZ NOT NULL,
  data JSONB
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_04 PARTITION OF events
  FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
  • Range: 시간, 숫자 범위.
  • List: 국가 코드, 카테고리.
  • Hash: 고른 분포가 필요할 때.

이점: 파티션 프루닝(필요 없는 파티션 스킵), 부분 인덱스, 오래된 파티션 빠른 DROP.

샤딩 (여러 DB로)

Citus(PostgreSQL 확장, 이제 Microsoft 소유)가 대표. 샤드 키 기반으로 여러 워커 노드에 분산.

수동 샤딩의 함정:

  • 샤드 간 JOIN 불가 또는 매우 느림.
  • 샤드 간 트랜잭션 — 앞 글(분산)에서 본 그 문제.
  • 리샤딩은 악몽.

Part 8 — 실무에서 DB가 느려지는 7가지 이유

  1. Dead Tuple 축적SELECT relname, n_dead_tup FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
  2. 인덱스 블로트pg_stat_user_indexes.idx_blks_hit이 낮으면 의심.
  3. 통계 오래됨 — ANALYZE 실행, EXPLAIN의 행수 예측 비교.
  4. 플랜 캐시 오염 — 같은 SQL이 파라미터에 따라 다른 플랜이 필요한데 캐시된 플랜 사용.
  5. 연결 폭주 — PostgreSQL은 프로세스 기반, 연결당 메모리 큼. PgBouncer 필수.
  6. 체크포인트 I/O 폭주checkpoint_timeout, max_wal_size 튜닝.
  7. 긴 트랜잭션이 Vacuum을 막음SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';

Part 9 — 2025년 DB 생태계 지형

관계형 DBMS

  • PostgreSQL 17 (2024): incremental backup, bi-directional logical replication, JSON_TABLE.
  • MySQL 8.4 LTS (2024): Group Replication, MySQL Shell GA.
  • SQL Server 2022: Azure Synapse Link, Ledger 테이블.

분석 DB

  • DuckDB 1.0 (2024): 로컬 OLAP의 혁명. Pandas보다 10-100배 빠름.
  • ClickHouse: 여전히 웹 규모 OLAP의 왕.
  • Apache Datafusion + Arrow: Rust 기반 임베디드 분석 엔진.

분산 SQL

  • CockroachDB 24.x: SQL 호환성 크게 향상.
  • TiDB 8.x: 중국·동남아 시장 장악.
  • YugabyteDB 2.20+: PostgreSQL 100% 호환 행보 강화.

벡터 DB

  • pgvector 0.8: HNSW + halfvec(16비트) + 양자화.
  • Qdrant, Weaviate, LanceDB: 네이티브 벡터 DB 경쟁.
  • Milvus 2.4: 대규모 벡터 특화.

임베디드

  • SQLite 3.46: 2025년에도 "가장 많이 배포된 DB"는 SQLite.
  • DuckDB: 분석용 SQLite.
  • libSQL (Turso): SQLite 포크, 엣지 DB.

Part 10 — 실무 체크리스트 (12항목)

  1. EXPLAIN (ANALYZE, BUFFERS)를 읽을 줄 알아야 한다 — 쿼리 튜닝의 시작.
  2. **연결 풀(PgBouncer, RDS Proxy)**을 반드시 도입 — PG는 연결당 비용이 크다.
  3. autovacuum을 끄지 마라 — 대신 파라미터 튜닝.
  4. 인덱스는 '꼭 필요할 때만' — 쓰기 비용이 선형 증가.
  5. UPDATE가 많은 테이블은 HOT 업데이트를 활용 — 인덱스 컬럼 외 변경.
  6. random_page_cost를 SSD에 맞게 낮춰라 — 기본값 4.0은 HDD 기준.
  7. 긴 트랜잭션을 피하라 — Vacuum을 방해한다.
  8. 복제 지연을 SLI로 추적하라 — 읽기 복제본 쓰면 필수.
  9. 큰 삭제는 배치로 — 한 번에 수백만 행 DELETE = WAL 폭발.
  10. 백업 복구를 정기 훈련 — 존재만 아니라 작동 확인.
  11. **스키마 변경은 pg_repack/pt-online-schema-change**로 무중단.
  12. 벡터는 HNSW + 적절한 m/ef_construction — 차원과 데이터 크기에 맞게.

Part 11 — 10대 안티패턴

  1. SELECT * — 불필요한 I/O, 뷰 망침.
  2. OFFSET 100000 — 깊은 페이지네이션은 Cursor 기반으로.
  3. N+1 쿼리 — ORM의 고전적 함정.
  4. LIKE '%pattern%' — 인덱스 못 쓴다. pg_trgm GIN 인덱스를 쓰라.
  5. UUID v4 기본키 — B-Tree 페이지 분할 폭주. v7(2024)은 시간 정렬.
  6. 모든 컬럼에 인덱스 — 쓰기 비용 폭증.
  7. 트랜잭션 안에 외부 API 호출 — 커넥션 점유, 데드락 유발.
  8. autovacuum 비활성화 — 대부분 증상을 더 악화시킨다.
  9. 잦은 소규모 UPDATE로 테이블 블로트 — 배치화/partitioning/append-only 고려.
  10. EXPLAIN 없이 "인덱스 탔을 거야"라 믿기 — 진짜 원인은 통계일 때가 많다.

마치며 — DB는 '시스템 엔지니어링의 결정체'

데이터베이스만큼 컴퓨터 과학의 모든 기초가 응집된 소프트웨어는 드물다. 자료구조(B-Tree/LSM), 시스템(파일 시스템/메모리), 분산(앞 글 전체), 최적화(CBO), 동시성(MVCC), 언어(SQL)…

한 명의 엔지니어가 DB를 이해하면, 사실상 백엔드의 모든 영역을 이해할 기반을 갖춘다. 그래서 시니어 엔지니어가 DB에 시간을 쓰는 것이다. 쿼리 하나를 튜닝하는 일이 아키텍처 전체를 재평가하는 기회가 된다.

2025년, 당신이 쓰는 앱 뒤엔 반드시 DB가 있다. 그 DB의 내부를 이해하면, 당신은 "DB 오너"가 된다. 이해 못 하면, 당신은 "DB 희생자"가 된다.

다음 글 예고 — "메시지 큐와 이벤트 스트리밍의 모든 것" — Kafka, RabbitMQ, NATS, Pulsar, SQS, Redis Streams 완전 비교

DB는 '상태'를 다루고, 메시지 큐/스트림은 '흐름'을 다룬다. 현대 아키텍처의 절반은 이 '흐름'에 있다.

  • 메시지 큐 vs 이벤트 스트림 — RabbitMQ와 Kafka의 근본 차이
  • Kafka 아키텍처 내부 — 파티션, 세그먼트, ISR, 리밸런싱
  • NATS JetStream — 가볍지만 강력한 경쟁자
  • Apache Pulsar — 지오리플리케이션의 왕
  • AWS SQS vs SNS vs Kinesis vs EventBridge — 각각 언제?
  • Redis Streams — 작은 규모에서 Kafka 대체
  • Exactly-Once 실현법 — 이전 글에서 언급한 이론을 실제 구현으로
  • CDC(Change Data Capture) — DB와 스트림의 연결, Debezium/Fivetran
  • Schema Registry, Avro, Protobuf — 스키마 진화의 중요성
  • 이벤트 주도 아키텍처의 함정 — Event Sourcing이 늘 옳은 선택이 아닌 이유

"어떻게 대규모 이벤트를 안정적으로 흘려보낼 것인가?"의 교과서. — 다음 글에서.