본문으로 바로가기
COMMUNITY모든 이야기

데이터베이스는 왜 '느린 디스크'에서 0.1ms에 답하는가

빗방울 2026.10.30 20:56 9분 읽기 조회 8 · 추천 2

여러분이 검색창에 무언가를 입력하면, 그 답은 디스크에서 나옵니다. 디스크는 느립니다. SSD도 메모리보다 백 배 이상 느립니다. 그런데 데이터베이스는 수백만 건 중에서 0.1밀리초 만에 한 줄을 찾아냅니다.

어떻게 가능할까요. 답은 '빠른 디스크를 써서'가 아닙니다. 디스크를 거의 읽지 않는 구조를 만들었기 때문입니다. 그 구조가 어떻게 생겼는지가 이 글의 주제입니다.

전제: 모든 것이 페이지 단위다

데이터베이스는 바이트를 하나씩 읽지 않습니다. 페이지(page)라는 고정 크기 덩어리로 읽습니다. 보통 8KB 또는 16KB입니다. 디스크도, 운영체제도, 파일시스템도 이 단위로 움직입니다.

이 사실이 모든 설계를 결정합니다. 한 번에 8KB를 읽는 비용은, 그 안에서 1바이트를 쓰든 8KB를 다 쓰든 같습니다. 그래서 데이터베이스는 '한 번 읽을 때 최대한 많은 정보를 뽑아내는' 방향으로 진화했습니다.

1층: B+ 트리 — 디스크를 위한 나무

인덱스의 기본값은 B+ 트리입니다. 이진 탐색 트리(BST)와 목적은 같습니다. 다만 디스크에 맞게 변형됐습니다.

BST는 값 하나마다 노드 하나입니다. 100만 건이면 노드 100만 개이고, 트리 높이가 20쯤 됩니다. 노드마다 디스크를 읽으면 20번의 I/O가 필요합니다. 느립니다.

B+ 트리는 노드 하나에 수백 개의 키를 담습니다. 8KB 페이지에 키와 포인터를 채워 넣으면, 팬아웃(fan-out)이 수백이 됩니다. 그러면 3~4단만으로 수억 건을 커버합니다.

계산해 봅시다. 팬아웃 500, 깊이 4라면 잎 노드가 500⁴ = 625억 개입니다. 즉 디스크를 4번만 읽으면 수십억 건 중 하나를 찾습니다. 게다가 상위 몇 단은 메모리에 캐시되므로 실제 I/O는 2~3회로 줄어듭니다.

잎 노드들이 연결 리스트로 이어져 있는 것도 중요합니다. 범위 검색(WHERE date BETWEEN ...)에서 첫 잎을 찾은 뒤 옆으로 걸어가면 되기 때문입니다.

2층: 버퍼 풀 — 메모리가 디스크인 척한다

B+ 트리가 4번이면 충분하다고 했지만, 초당 수만 건이 들어오면 그것도 부담입니다. 그래서 데이터베이스는 버퍼 풀을 둡니다.

버퍼 풀은 '디스크 페이지를 메모리에 올려둔 캐시'입니다. 페이지를 읽을 때 먼저 버퍼 풀을 확인합니다. 있으면 메모리에서 즉시 반환합니다. 없으면 디스크에서 읽어 올려둡니다.

여기서 중요한 트레이드오프가 나옵니다. 메모리가 크면 캐시 적중률이 올라가 빨라집니다. 그런데 메모리가 무한하지 않으므로 무엇을 내보낼지 결정해야 합니다. 대부분 LRU(최근 사용 안 한 것부터) 변형을 씁니다.

실무 감각으로 말하면, 버퍼 풀 크기가 데이터셋보다 크면 거의 모든 읽기가 메모리에서 끝납니다. 반대로 작으면 매번 디스크로 떨어져 체감 속도가 급격히 떨어집니다.

3층: 쓰기 — WAL이 '느린 쓰기'를 감춘다

읽기만 빠르면 반쪽입니다. 쓰기에는 더 큰 문제가 있습니다. 데이터를 고치면 B+ 트리의 여러 페이지가 바뀝니다. 페이지를 디스크의 원래 위치에 바로 쓰면, 도중에 전원이 나갔을 때 트리가 깨집니다.

그래서 대부분 데이터베이스는 WAL(Write-Ahead Log)을 씁니다. 순서는 이렇습니다.

1) 변경 내용을 로그에 먼저 append하고 디스크에 확정(fsync)합니다.
2) 실제 페이지는 메모리에서 고칩니다(dirty page).
3) 나중에 백그라운드가 dirty page를 디스크로 내려씁니다(checkpoint).

이렇게 하면 로그는 순차 쓰기가 됩니다. 디스크에서 순차 쓰기는 임의 쓰기보다 훨씬 빠릅니다. 사용자는 '로그가 확정된 순간' 커밋이 완료됐다고 통보받고, 실제 데이터 페이지 정리는 나중에 이뤄집니다.

장애가 나면? 로그를 재생(replay)해서 커밋된 변경만 복원합니다. 이것이 원자성(atomicity)의 실체입니다.

4층: 트랜잭션 — 동시성과 격리

여러 사람이 동시에 같은 행을 고치면 어떻게 될까요. 고전적 해법은 락(lock)입니다. 읽거나 쓰는 동안 다른 접근을 막습니다. 확실하지만 느립니다. 읽기끼리도 서로 기다려야 하니까요.

그래서 널리 쓰이는 방식이 MVCC(다중 버전 동시성 제어)입니다. 핵심 발상은 단순합니다. 덮어쓰지 말고, 새 버전을 옆에 만들자.

행을 고치면 기존 행을 지우는 대신 새 버전을 추가하고, 각 버전에 생성 시점과 소멸 시점을 기록합니다. 읽는 쪽은 자기 트랜잭션 시작 시점에 유효한 버전을 봅니다.

결과적으로 읽기는 쓰기를 기다리지 않고, 쓰기도 읽기를 기다리지 않습니다. 대신 단점이 있습니다. 오래된 버전이 쌓여 공간이 불어나고, 필요 없는 버전을 청소하는 vacuum 작업이 따라붙습니다.

5층: 인덱스는 공짜가 아니다

인덱스를 걸면 읽기가 빨라집니다. 그런데 쓰기가 느려집니다. INSERT 한 번에 모든 인덱스에 항목을 추가해야 하니까요.

또 다른 함정이 카디널리티입니다. 성별(2종류) 같은 컬럼에 인덱스를 걸면, 조건에 해당하는 행이 전체의 절반입니다. 그러면 인덱스로 절반을 찾은 뒤 원본 페이지를 절반이나 다시 읽어야 합니다. 이때 옵티마이저는 인덱스를 버리고 전체 스캔을 택합니다. 인덱스가 있어도 안 쓰이는 것입니다.

그래서 복합 인덱스의 컬럼 순서가 중요합니다. (a, b) 인덱스는 a로 먼저 좁히고 그 안에서 b를 씁니다. b만 조건이면 이 인덱스는 거의 쓸모가 없습니다.

6층: 옵티마이저 — '무엇이 빠른가'를 계산한다

SQL은 선언이고, 실행 계획은 선택입니다. 같은 결과를 내는 방법이 여러 개일 때, 옵티마이저는 비용을 추정해 고릅니다.

비용 추정의 근거는 통계 정보입니다. 행 수, 컬럼별 값 분포(히스토그램), 인덱스의 선택도 같은 것들입니다. 이 통계가 낡으면 판단이 틀립니다. 실제로는 10건인데 100만 건으로 추정하면, 옵티마이저는 인덱스를 버리고 전체 스캔을 고릅니다. 그래서 정기적인 통계 갱신(ANALYZE)이 필요합니다.

7층: '느리다'를 찾는 법

실무에서 느린 쿼리를 만나면, 원인은 대개 다음 중 하나입니다.

· 인덱스가 없어 전체 스캔 — 실행 계획의 type이 ALL인지 확인
· 인덱스는 있는데 안 씀 — 카디널리티가 낮거나 통계가 낡았거나, 컬럼에 함수를 적용했기 때문(WHERE YEAR(created_at)=2026)
· 버퍼 풀이 작음 — 디스크 I/O가 급증
· N+1 쿼리 — 애플리케이션이 목록 100건에 쿼리 101번을 날림
· 락 대기 — 동시성이 높아 서로 기다림

반전: 데이터베이스의 진짜 천재성은 '포기'에 있다

여기까지 읽으면 모든 것을 다 잘하려는 시스템처럼 보입니다. 그런데 실제 설계의 핵심은 무엇을 포기했는지입니다.

관계형 데이터베이스는 일관성과 내구성을 위해 수평 확장성을 포기했습니다. 반대로 많은 NoSQL은 가용성과 확장성을 위해 강한 일관성을 포기합니다. CAP 정리가 말하는 것이 바로 이 트레이드오프입니다.

그래서 '어떤 DB가 최고인가'라는 질문은 답이 없습니다. '무엇을 포기할 수 있는가'가 질문입니다. 은행 잔고는 일관성을 포기할 수 없고, 좋아요 개수는 몇 초 늦어도 됩니다.

그래서 앞으로 뭘 보면 되나

첫째, 실행 계획입니다. 느린 쿼리를 만나면 가장 먼저 EXPLAIN을 보세요. 추측보다 계획이 정확합니다.

둘째, 버퍼 풀 적중률입니다. 디스크를 읽는 비율이 올라가면 메모리가 부족하다는 신호입니다.

셋째, 쓰기 증폭입니다. 인덱스가 많을수록 한 번의 INSERT가 여러 페이지를 건드립니다.

여러분은 어떤 데이터베이스를 쓰시나요? 최근에 '왜 이게 느리지' 하고 막혔던 경험이 있다면 댓글로 남겨주세요. 실행 계획을 같이 볼 수도 있습니다.

참고 자료

  • B+ 트리의 팬아웃과 디스크 I/O 횟수 — 데이터베이스 시스템 교과서(스토리지 엔진 장)
  • WAL(Write-Ahead Logging)과 체크포인트 — 트랜잭션 내구성 구현
  • MVCC와 vacuum — 다중 버전 동시성 제어
  • 본 글은 특정 제품(PostgreSQL, MySQL 등)에 한정하지 않고 공통 원리를 정리한 것입니다. 세부 구현은 제품마다 다릅니다.

모든 이야기1502

가벼운 안부부터 깊은 생각까지, 함께 나눠요.

글쓰기
총 1502개의 이야기
댓글