유성

Undo Log vs Tuple Versioning: MVCC로 분석하는 MySQL과 PostgreSQL의 결정적 차이 본문

Database

Undo Log vs Tuple Versioning: MVCC로 분석하는 MySQL과 PostgreSQL의 결정적 차이

백엔드 유성 2026. 2. 4. 17:03

DB를 선택할 때 보통 "MySQL은 웹 서비스에 좋고, PostgreSQL은 복잡한 데이터 분석에 유리하다"는 말을 듣곤 한다.

물론 맞는 이야기다.

 

안정성을 위해 멀티 프로세스(PG)를 쓰는지, 속도를 위해 멀티 스레드(MySQL)을 쓰느냐의 차이도 있겠지만,

이 글에서는 가장 결정적인 차이 Index와 MVCC를 구현하는 방식에 대해서 알아본다.

 

만약 MVCC가 무엇인지 궁금하다면, 아래 글을 참고하자.

 

데이터베이스 격리 수준과 동시성 제어의 실체

REPEATABLE READ는 어떻게 구현되는가?부제: MySQL InnoDB의 MVCC와 Undo Log 해부 트랜잭션 격리 수준은 데이터베이스의 동시성 처리 성능과 데이터 무결성 사이의 트레이드오프를 결정하는 핵심 설정이다

youseong.tistory.com

 

 

1. MySQL (InnoDB): 최신 데이터는 내 자리에, 과거는 Undo Log에

MySQL의 InnoDB 엔진은 UPDATE 시 기존 데이터를 덮어쓰는 방식을 사용한다.

 

동작 원리: Undo Log 활용

  1. 복사: 데이터를 변경하기 전, 기존 데이터를 Undo Log라는 별도의 공간에 기록한다.
  2. 갱신: 실제 Row 위치에는 신규 데이터를 넣는다.
  3. 읽기 과정
    1. 조건에 맞는 데이터를 조회할 때 가장 먼저 신규 데이터를 읽는다.
    2. 이때 데이터의 트랜잭션 ID를 확인하여, 현재 내 트랜잭션이 읽을 수 있는 버전인지 비교한다.
    3. 만약 내가 읽을 수 없는 버전이라면, 연결된 Undo Log를 타고 과거 데이터를 추적하며 비교 과정을 반복한다. 

비유: HashMap과 LinkedList

이 과정은 자료구조의 HashMap에서 해시 충돌이 발생했을 때와 매우 흡사하다.

최신 데이터(Hash Key)를 먼저 확인하고, 원하는 데이터가 아니면 내부에 연결된 LinkedList를 타고 들어가며 조건에 맞는 데이터를 찾는 과정과 같기 때문이다.

 

사용하지 않는 Undo Log는?

Undo Log를 사용하는 TID가 없는 경우에는 이를 백그라운드 스레드가 알아서 삭제(Purge) 시킨다.

그렇기에 데이터 파일 자체가 작고 깔끔하게 유지되는 특성이 있다.

 

MySQL 성능 요약 (Row 위치 검색 비용 제외)

  • 쓰기/수정: O(1)로 Undo Log를 생성하고 실제 Row를 덮어쓰기만 하면 되므로 매우 빠르다.
  • 최신 데이터 읽기: O(1)로 원본 데이터가 항상 최신이므로 Undo Log가 아무리 많아도 즉시 읽게 된다.
  • 과거 데이터 읽기: O(n)으로 Version Chain(Undo Log 연결 리스트)의 길이에 비례하여 성능이 저하된다.
  • 특징: 실제 Row 영역을 작게 유지하여 인덱스 효율을 극대화한다.

쓰기 과정에서 Clustering Index 수정이 필요한가?

PK가 아닌 다른 컬럼의 데이터를 수정하는 경우, 파일에서 값만 수정되므로 인덱스 수정은 필요 없다.

PK는 파일 내에서도 순서대로 배치되므로 성능을 높이고자 한다면 INSERT할때 PK를 순서대로(Auto-increment) 넣는것이 효율적이다.

 

 

2. PostgreSQL: 과거는 그대로 두고, 새 집을 만든다.

PostgreSQL은 데이터를 수정할 때 기존 데이터를 건드리지 않고 새로운 위치에 새 버전의 데이터를 하나 더 만든다.

 

동작 원리: Tuple Versioning

  1. 생성: 업데이트가 발생하면 기존 데이터는 그대로 두고, 새로운 데이터를 빈 공간에 삽입한다.
  2. 마킹: 기존 데이터에는 "이 데이터는 몇 번 트랜잭션에 의해 삭제/수정됨"이라는 표시인 xmax만 남긴다.
  3. 읽기 과정
    1. 테이블 내의 여러 버전의 튜플들을 마주하게 되며, 자신의 스냅샷과 비교하여 읽을 수 있는 최신 버전을 선택한다.
    2. MySQL처럼 로그를 찾아 헤매지 않고, 테이블 내에 존재하는 튜플로 직접 접근한다.

쉽게 표현하면, PK가 1인 철수 이름을 훈이로 UPDATE 하고 TID는 3이라면

UPDATE 후 테이블에는 아래와 같이 데이터가 남는 것이다.

id(PK) name xmax
1 철수 3
1 훈이  

 

기존 데이터를 삭제하지 않는다면 문제가 있지 않을까?

기존 데이터가 삭제되지 않기 때문에 실제 테이블을 차지하는 공간이 커진다는 문제가 발생한다.

이 문제는 단순히 데이터 파일 크기 문제 뿐 아니라 읽기 성능에도 영향을 주게된다.

만약 Long Query가 발생해서, xmax 를 제때 삭제하지 못하면 테이블의 크기가 커지는 Bloat(부풀기) 현상이 발생할 수 있다.

 

또한, 이 방식은 데이터들은 순서대로(예: Clustering Index) 배치하기 어렵기 때문에 PK에 대한 Index 페이지를 활용한다.

Index 페이지는 파일 주소를 가르키는데, 위에서 본 것 처럼 UPDATE 쿼리가 덮어쓰기가 아니므로 파일의 주소 변경이 발생하고,

이말의 뜻은 Index 페이지의 수정이 발생한다는 것이다.

 

PostgreSQL 성능 요약 (Tuple 위치 검색 비용 제외)

  • 쓰기/수정: O(1)로 파일 덮어쓰기가 아닌 쓰기가 수행된다. 인덱스 업데이트 비용이 크다.
  • 최신 데이터 읽기: O(1)로 원본 데이터를 바로 찾아 읽는다.
  • 과거 데이터 읽기: O(1)로 원본 데이터를 바로 찾아 읽는다.
  • 정리: 더 이상 아무도 읽지 않는 Dead Tuple이 쌓이므로 VACUUM이라는 백그라운드 프로세스가 주기적으로 청소해준다.

 

 

3. 마치며, 그래서 무엇을 선택해야 할까?

지금까지 살펴본 MySQL(Undo Log 기반)과 PostgreSQL(Tuple Versioning 기반)의 차이는 결국

과거 데이터를 관리하는 비용을 누가, 언제 지불하느냐의 차이로 요약할 수 있다.

 

어떠 상황에 어떤 DB 가 유리할까?

 

MySQL이 유리한 경우: 빠른 응답속도가 필요한 웹 서비스

  • Update가 빈번한 환경: PK가 아닌 컬럼을 자주 수정한다면, 인덱스 재구성 비용이 적은 MySQL이 압도적으로 유리하다.
  • 최신 데이터 위주의 조회: 대부분 웹 서비스처럼 최신 게시글, 현재 상태 등을 빠르게 읽어야 한다면 MySQL이 효율적이다.
  • 저장 공간 최적화: 실제 Row 영역을 작게 유지하므로, 대량의 데이터를 제한된 디스크 자원으로 운영할 때 유리하다.

PostgreSQL이 유리한 경우: 깊고 복잡한 쿼리가 필요한 분석 환경

  • 복잡한 쿼리와 조인: 여러 테이블을 조인하고 대량의 데이터를 분석할 때 O(1)의 버전 접근성과 똑똑한 옵티마이저가 빛을 발휘한다.
  • 과거 데이터 분석: Long Query가 돌아가더라도 Undo Log같은 체인을 타지 않으므로, 긴 시간 동안 실행되는 분석 쿼리에 강점이 있다.
  • 데이터 무결성과 기능성: JSONB, GIS, 복잡한 제약 조건 등 단순 저장 이상의 기능이 필요할 때 더 많은 기능을 제공한다.

 

물론 서비스가 아주 작지 않은 이상 어떤것이 좋다고 딱 떨어지지 않을 것이다.

위에서 설명한 특성들을 서비스에 대입하여 어떻게 동작할지 미리 시뮬레이션을 해보고 데이터베이스를 적용하는 것이 바람직하다.