cd ../
Database·2026-03-18·6 min read·# entry/028

MVCC 깊이 파기: Oracle SCN 비교 vs PostgreSQL 행 버전 관리

Oracle의 Undo 기반 MVCC와 PostgreSQL의 행 버전 관리를 SCN·xmin·xmax 메커니즘 수준에서 비교합니다.

쿼리가 실행되는 도중, 다른 트랜잭션이 동일한 데이터를 수정하고 커밋해버린다면 어떻게 될까요?

읽기 시작 시점의 값과 읽기 완료 시점의 값이 달라지는 일관성 깨짐 문제입니다. RDBMS는 이를 MVCC (Multi-Version Concurrency Control) 로 해결하는데, Oracle과 PostgreSQL은 서로 다른 전략을 씁니다.


1. MVCC란?

MVCC의 핵심 아이디어는 단순합니다.

데이터를 덮어쓰지 말고, 여러 버전을 동시에 유지하라.

쓰기 트랜잭션이 기존 버전을 보존한 채 수정하면, 읽기 트랜잭션은 락 없이 자신이 봐야 할 시점의 버전을 골라 읽을 수 있습니다. 읽기와 쓰기가 서로를 블로킹하지 않습니다.

일관성(Consistency) vs 최신성(Currency)

두 개념은 트레이드오프 관계입니다.

  • 일관성: 쿼리가 시작된 시점 기준으로 데이터가 균일하게 보이는 것
  • 최신성: 현재 커밋된 가장 최신 값을 보는 것

MVCC는 일관성을 우선합니다. "쿼리 시작 시점의 스냅샷"이야말로 유일하게 신뢰할 수 있는 정답이기 때문입니다.


2. Oracle: Undo 기반 MVCC

Oracle은 최신 버전 데이터를 데이터 파일에 직접 저장하고, 이전 버전을 별도의 Undo 세그먼트에 보관합니다.

SCN이란?

SCN (System Commit Number)은 커밋이 발생할 때마다 Oracle이 증가시키는 글로벌 타임스탬프입니다. 각 데이터 블록은 ITL(Interested Transaction List)에 마지막으로 수정된 커밋의 SCN을 기록해 둡니다.

SCN은 Git의 커밋 해시와 유사합니다. "이 블록이 언제 마지막으로 바뀌었는가"를 숫자 하나로 추적합니다.

Consistent 모드 동작 원리

1

쿼리 SCN 기록

쿼리가 시작되는 순간 현재 SCN을 쿼리 SCN으로 기록합니다. 이 시점이 "나의 현재"가 됩니다.

2

블록 SCN 비교

데이터 블록을 읽을 때마다 블록의 블록 SCN과 쿼리 SCN을 비교합니다.

  • 블록 SCN ≤ 쿼리 SCN → 쿼리 시작 이전에 수정된 블록. 그대로 읽습니다.
  • 블록 SCN > 쿼리 SCN → 쿼리 시작 이후에 수정된 블록. CR Copy를 만듭니다.
3

CR Copy 생성 (Consistent Read Copy)

해당 블록을 메모리에 복사한 뒤, Undo 세그먼트에서 변경 전 값을 꺼내 롤백을 적용합니다. 이 복사본이 쿼리 시작 시점의 모습입니다. 실제 디스크의 블록은 건드리지 않습니다.

4

복사본에서 읽음

이후 해당 블록에 대한 읽기는 메모리의 CR Copy를 통해 이루어집니다.

Snapshot Too Old (ORA-01555)

Undo 세그먼트는 무한정 데이터를 보관하지 않습니다. 쿼리 실행 중 Undo 데이터가 덮어써지면 CR Copy를 만들지 못하고 ORA-01555: snapshot too old 에러가 발생합니다. UNDO_RETENTION 파라미터로 보존 기간을 늘려 완화할 수 있습니다.


3. PostgreSQL: 행 버전 관리 (Heap-based MVCC)

PostgreSQL은 별도의 Undo 공간 없이, 데이터 페이지 안에 여러 버전의 행을 직접 유지합니다.

각 행의 숨겨진 메타데이터

모든 Tuple(행)에는 두 개의 숨겨진 필드가 있습니다.

필드의미
xmin이 행을 삽입·수정한 트랜잭션 ID
xmax이 행을 삭제·업데이트한 트랜잭션 ID (없으면 0)

UPDATE 동작 원리

PostgreSQL에서 UPDATE는 물리적으로 DELETE + INSERT입니다.

1

기존 행 만료 처리

기존 행의 xmax를 현재 트랜잭션 ID로 채웁니다. 이 행은 "이 트랜잭션 이후에는 죽은 행"으로 취급됩니다.

2

새 행 삽입

수정된 값을 가진 새 행을 같은 페이지(또는 새 페이지)에 추가합니다. 새 행의 xmin은 현재 트랜잭션 ID로 설정됩니다.

3

SELECT 시 버전 필터링

SELECT는 쿼리 시작 시점의 스냅샷 트랜잭션 ID를 기준으로, xmin ≤ 스냅샷 ID 이고 xmax = 0 또는 xmax > 스냅샷 ID 인 행만 읽습니다.

VACUUM의 역할

UPDATE/DELETE로 생긴 dead tuple(죽은 행)은 즉시 지워지지 않습니다. AUTOVACUUM이 주기적으로 dead tuple을 회수하여 페이지 공간을 재활용합니다. VACUUM 튜닝이 PostgreSQL 성능에 직접적인 영향을 미치는 이유입니다.


4. Oracle vs PostgreSQL 비교

데이터 저장: 최신 버전만 데이터 파일에 보관, 이전 버전은 Undo 세그먼트에 분리 저장

읽기: 과거 버전 조회 시 Undo 체인을 역추적 → 긴 트랜잭션에서 비용 증가

쓰기: 데이터 파일은 in-place update, 변경 전 값만 Undo에 기록

공간 관리: Undo 세그먼트는 자동 재사용 → 별도 VACUUM 불필요

Oracle의 강점
  • 데이터 파일에 항상 최신 버전만 유지 → 스토리지 bloat 없음
  • 별도 VACUUM 프로세스 불필요
  • Undo 체인이 짧다면 읽기 성능이 안정적
Oracle의 약점
  • Undo 세그먼트 부족 시 Snapshot Too Old 에러
  • Undo 세그먼트 사이징이 DBA의 숙제
  • 매우 긴 읽기 트랜잭션에서 Undo 체인이 길어질수록 성능 저하
PostgreSQL의 강점
  • 구조가 단순 — 별도 Undo 공간 불필요
  • 과거 버전 접근 비용이 낮음 (같은 페이지 내 필터링)
  • Snapshot Too Old 류의 에러 없음
PostgreSQL의 약점
  • 빈번한 UPDATE/DELETE 시 dead tuple 누적으로 테이블 bloat
  • AUTOVACUUM 튜닝이 성능에 직접적인 영향
  • UPDATE가 새 행을 물리적으로 쓰므로 쓰기 증폭(Write Amplification) 발생

핵심 요약

정리
  • MVCC는 여러 버전을 동시에 유지하여 읽기와 쓰기가 서로를 블로킹하지 않게 합니다.
  • Oracle은 최신 데이터를 파일에, 이전 버전을 Undo 세그먼트에 분리 보관합니다. SCN 비교로 필요한 시점을 판별하고 CR Copy를 생성해 읽습니다.
  • PostgreSQL은 모든 버전을 같은 페이지에 공존시키고, xmin/xmax로 각 행의 가시성을 판별합니다. 죽은 행은 VACUUM이 정리합니다.
  • 어떤 방식이든 목표는 같습니다: "쿼리 시작 시점의 일관된 스냅샷을 제공한다."