4P by GN⁺ | ★ favorite | 댓글 1개
  • PostgreSQL은 최근 인터넷의 기본 선택지처럼 떠올랐지만, Carnegie Mellon과 OtterTune의 경험상 MVCC 구현은 MySQL, Oracle, Microsoft SQL Server보다 운영 부담이 큼
  • 행을 갱신할 때 기존 행을 덮어쓰지 않고 새 물리 버전을 만드는 append-only 저장 방식과 오래된 버전에서 새 버전으로 따라가는 O2N 체인이 핵심 설계임
  • 이 구조는 전체 튜플 복사, dead tuple 누적, 모든 인덱스 갱신, autovacuum 의존으로 이어지며 HOT 업데이트가 가능한 경우에만 일부 비용을 피할 수 있음
  • OtterTune 고객 PostgreSQL 분석에서는 평균 약 46%의 업데이트만 HOT 최적화를 사용했고, 기본 autovacuum 설정은 큰 테이블에서 정리를 오래 늦출 수 있음
  • PostgreSQL은 여전히 매력적인 DBMS지만, 쓰기 많은 워크로드와 큰 테이블에서는 저장 공간, I/O, 메모리, 인덱스 유지보수, vacuum 운영을 직접 관리해야 함

PostgreSQL MVCC가 문제가 되는 이유

  • 데이터베이스 선택지는 2023년 4월 기준 DBDB에 897개가 등록될 만큼 많지만, 시기별로 사실상의 기본 선택지가 있었음
    • 2000년대에는 Google과 Facebook이 쓰던 MySQL이 관례적 선택지였음
    • 2010년대에는 MongoDB가 “webscale” 이미지와 함께 주목받음
    • 최근 5년간은 PostgreSQL이 안정성, 풍부한 기능, 확장성, 운영 워크로드 적합성 때문에 널리 선호됨
  • 글의 초점은 PostgreSQL의 다중 버전 동시성 제어(MVCC) 구현임
    • Carnegie Mellon의 연구와 Amazon RDS PostgreSQL 최적화 경험에서 PostgreSQL의 MVCC 구현은 MySQL, Oracle, Microsoft SQL Server보다 나쁘다고 평가됨
    • Amazon Aurora PostgreSQL도 같은 구조적 문제를 가짐

MVCC의 목적과 PostgreSQL의 설계 선택

  • MVCC는 여러 쿼리가 가능하면 서로 방해하지 않고 동시에 데이터베이스를 읽고 쓰게 만들기 위한 방식임
    • DBMS는 기존 행을 덮어쓰지 않고 논리적 행마다 여러 물리 버전을 유지함
    • 쿼리는 생성 시각 같은 버전 순서에 따라 자신에게 맞는 버전을 읽음
    • 트랜잭션 시작 시점의 데이터베이스 스냅샷을 보는 스냅샷 격리가 가능해짐
  • 읽기 작업은 같은 항목을 갱신하는 쓰기 작업 때문에 명시적 레코드 락에 막히는 상황을 줄일 수 있음
  • MVCC DBMS는 크게 세 가지를 결정해야 함
    • 기존 행의 업데이트를 어떻게 저장할지
    • 실행 시점에 쿼리에 맞는 행 버전을 어떻게 찾을지
    • 더 이상 보이지 않는 만료 버전을 어떻게 제거할지
  • PostgreSQL은 1980년대에 고른 첫 번째 선택 때문에 나머지 두 영역에서도 오늘날까지 부담을 안고 있음

append-only 버전 저장

  • PostgreSQL은 처음부터 다중 버전을 지원하도록 설계됐고, 갱신 시 기존 행을 덮어쓰지 않고 복사본을 만든 뒤 새 버전에 변경 사항을 적용함
  • 이 방식은 append-only 버전 저장으로 볼 수 있음
    • 기존 튜플을 업데이트하면 DBMS가 테이블에서 새 행 버전을 위한 빈 슬롯을 확보함
    • 현재 버전의 행 내용을 새 버전으로 복사함
    • 새로 할당된 버전 슬롯에 변경 사항을 적용함
  • 예시 테이블은 영화 정보를 담는 movies 테이블임
    • id는 기본 키이며 name, year, director 컬럼을 가짐
    • movies_pkey 기본 인덱스와 idx_name, idx_director 보조 B+Tree 인덱스가 있음
  • "Shaolin and Wu Tang"의 개봉 연도를 1985년에서 1983년으로 바꾸는 업데이트에서는 원래 튜플을 복사한 뒤 새 버전에 변경된 연도를 적용함
    • 기존 페이지에 공간이 없으면 새 버전은 다른 테이블 페이지에 만들어질 수 있음

버전 체인과 O2N 방식

  • 여러 물리 버전이 같은 논리 행을 나타내면 DBMS는 버전 간 관계를 기록해야 함
    • MVCC DBMS는 단방향 연결 리스트로 버전 체인을 만듦
    • 체인은 저장 공간과 유지보수 비용을 줄이기 위해 한 방향으로만 이어짐
  • 버전 체인 순서는 두 가지임
    • N2O: 최신 버전이 이전 버전을 가리키며, 체인의 머리가 항상 최신 버전임
    • O2N: 각 버전이 새 버전을 가리키며, 체인의 머리는 가장 오래된 버전임
  • Oracle과 MySQL을 포함한 대부분 DBMS는 N2O를 구현함
  • PostgreSQL은 Microsoft SQL Server의 In-Memory OLTP 엔진을 제외하면 드물게 O2N을 사용함
    • O2N은 튜플이 수정될 때마다 인덱스가 새 버전을 가리키도록 갱신할 필요를 줄일 수 있음
    • 대신 최신 버전을 찾기 위해 긴 버전 체인을 따라가야 할 수 있음
  • PostgreSQL 행 헤더의 t_tcid 필드는 다음 버전의 튜플 ID를 담거나, 최신 버전이면 자기 자신의 튜플 ID를 담음
    • 인덱스가 오래된 버전을 가리키면 PostgreSQL은 체인을 따라 새 버전을 찾아야 함

인덱스로 체인 탐색을 줄이는 방식

  • PostgreSQL 개발자들은 초기부터 두 가지 비용을 알고 있었음
    • 업데이트마다 전체 튜플의 새 복사본을 만드는 비용이 큼
    • 대부분의 쿼리가 원하는 최신 버전을 찾기 위해 전체 버전 체인을 탐색하는 일이 낭비임
  • PostgreSQL은 긴 체인 탐색을 피하려고 행의 각 물리 버전에 대해 테이블 인덱스 항목을 추가함
    • 논리 행 하나에 물리 버전이 5개 있으면 해당 튜플에 대해 인덱스 항목이 최대 5개 생길 수 있음
    • idx_name 인덱스가 "Shaolin and Wu Tang"의 여러 버전을 가리키면 PostgreSQL은 최신 버전에 직접 접근할 수 있음
  • 최신 버전 접근은 빨라질 수 있지만, 인덱스가 커지고 유지보수 비용이 늘어남

HOT 업데이트 최적화

  • PostgreSQL은 관련 버전이 여러 페이지에 흩어지고 여러 인덱스 항목이 생기는 상황을 줄이기 위해 HOT(heap-only tuple) 업데이트를 사용함
  • HOT 업데이트가 가능한 조건은 두 가지임
    • 업데이트가 테이블 인덱스에서 참조하는 컬럼을 수정하지 않음
    • 새 버전이 이전 버전과 같은 데이터 페이지에 저장될 수 있을 만큼 공간이 있음
  • HOT이 적용되면 인덱스는 여전히 오래된 버전을 가리키고, 쿼리는 버전 체인을 따라 최신 버전을 찾음
  • PostgreSQL은 정상 동작 중 오래된 버전을 제거해 버전 체인을 가지치기하는 최적화도 수행함

dead tuple 제거와 vacuum

  • PostgreSQL은 업데이트할 때마다 행 복사본을 만들기 때문에 오래된 버전인 dead tuple을 제거해야 함
  • 1980년대의 초기 PostgreSQL은 dead tuple을 제거하지 않았음
    • 오래된 버전을 유지하면 특정 시점의 데이터베이스 상태를 보는 time-travel 쿼리를 실행할 수 있다는 아이디어였음
    • 하지만 dead tuple을 제거하지 않으면 삭제가 있어도 테이블 크기가 줄지 않고, 자주 갱신되는 튜플의 버전 체인이 길어짐
  • PostgreSQL은 vacuum 절차로 테이블에서 dead tuple을 정리함
    • vacuum은 마지막 실행 이후 수정된 테이블 페이지를 순차 스캔하고 만료된 버전을 찾음
    • 어떤 버전이 활성 트랜잭션에 보이지 않으면 expired로 간주됨
    • 현재 트랜잭션이 그 버전을 접근하지 않고 미래 트랜잭션도 최신 live 버전을 쓰므로 공간 재사용이 안전함
  • PostgreSQL은 설정에 따라 autovacuum을 주기적으로 자동 실행함
    • 전역 설정으로 전체 테이블의 vacuum 빈도를 조정할 수 있음
    • 테이블 단위 autovacuum 설정도 가능함
    • 사용자는 VACUUM SQL 명령으로 수동 실행할 수 있음

문제 1: 전체 튜플 복사

  • append-only MVCC에서는 튜플의 한 컬럼만 바뀌어도 모든 컬럼을 새 버전으로 복사함
  • 이 방식은 데이터 중복과 저장 공간 요구량을 크게 늘림
    • PostgreSQL은 다른 DBMS보다 같은 데이터베이스를 저장하는 데 더 많은 메모리와 디스크가 필요할 수 있음
    • 결과적으로 쿼리가 느려지고 클라우드 비용이 증가할 수 있음
  • MySQL과 Oracle은 새 버전과 현재 버전 사이의 압축된 델타를 저장함
    • 1000개 컬럼이 있는 테이블에서 한 컬럼만 바뀌면 변경된 한 컬럼에 대한 델타 레코드만 저장함
    • PostgreSQL은 바뀐 한 컬럼과 바뀌지 않은 999개 컬럼이 포함된 새 버전을 만듦
  • PostgreSQL의 TOAST 속성은 다르게 처리되므로 이 비교에서는 제외됨
  • EnterpriseDB는 2013년 append-only 저장 엔진을 델타 버전 기반으로 바꾸려는 zheap 프로젝트를 시작했음
    • 마지막 공식 업데이트는 2021년의 상태 글
    • 이후 뚜렷한 진전은 보이지 않음

문제 2: 테이블 bloat

  • PostgreSQL의 만료 버전, 즉 dead tuple은 델타 버전보다 더 많은 공간을 차지함
  • write-heavy 워크로드에서는 autovacuum이 따라잡기 전에 dead tuple이 더 빨리 쌓일 수 있음
    • 테이블이 계속 커질 수 있음
    • dead tuple과 live tuple이 페이지 안에 섞여 있어 쿼리 실행 중 dead tuple도 메모리로 로드해야 함
    • 제어되지 않은 bloat는 테이블 스캔에서 IOPS와 메모리를 더 소비하게 만들어 쿼리 성능을 낮춤
    • dead tuple 때문에 옵티마이저 통계가 부정확해지면 나쁜 쿼리 계획으로 이어질 수 있음
  • 예시로 movies 테이블에 live tuple 1천만 개와 dead tuple 4천만 개가 있으면 80%가 obsolete 데이터임
    • 평균 튜플 크기가 1KB라면 live tuple은 10GB, dead tuple은 약 40GB를 차지함
    • 전체 테이블 크기는 50GB임
    • 전체 테이블 스캔 시 PostgreSQL은 대부분이 obsolete 데이터여도 디스크에서 50GB를 읽고 메모리에 올려야 함
  • PostgreSQL에는 순차 스캔이 버퍼 풀 캐시를 오염시키는 것을 피하는 보호 메커니즘이 있지만, I/O 비용 자체를 없애지는 못함

VACUUM과 VACUUM FULL의 차이

  • autovacuum이 정기적으로 실행되고 워크로드를 따라잡아도 일반 autovacuum은 저장 공간을 운영체제에 반환하지 못함
  • 일반 VACUUM은 dead tuple을 제거하고 각 페이지 안에서 live tuple을 재배치하지만, 디스크의 빈 페이지를 회수하지 않음
  • PostgreSQL이 마지막 페이지를 비울 수 있으면 해당 페이지를 잘라낼 수 있지만, 다른 페이지들은 디스크에 남음
    • 50GB 테이블에서 40GB의 dead tuple을 제거해도 PostgreSQL은 운영체제나 RDS 관점에서 50GB 할당 공간을 유지할 수 있음
  • 사용하지 않는 공간을 실제로 반환하려면 VACUUM FULL 또는 pg_repack으로 테이블 전체를 새 공간에 다시 써야 함
    • 두 작업은 리소스를 많이 쓰고 오래 걸림
    • 운영 데이터베이스에서는 쿼리 성능에 큰 영향을 줄 수 있음
  • VACUUM FULL은 각 페이지의 dead tuple을 제거하고 남은 live tuple을 새 페이지로 압축해 옮긴 뒤 불필요한 페이지를 삭제함

문제 3: 보조 인덱스 유지보수

  • PostgreSQL은 튜플 하나를 업데이트할 때 해당 테이블의 모든 인덱스를 업데이트해야 함
  • 기본 인덱스와 보조 인덱스 모두 버전의 정확한 물리 위치를 저장하기 때문임
    • 새 버전이 이전 버전과 같은 페이지에 저장되는 HOT 업데이트가 아니면 매 업데이트마다 이 작업이 필요함
  • 예시 업데이트에서는 PostgreSQL이 새 버전을 Table Page #2에 만든 뒤 movies_pkey, idx_director, idx_name에 새 버전을 가리키는 항목을 삽입함
  • 모든 인덱스를 수정해야 하는 구조는 여러 성능 비용을 만듦
    • 업데이트 쿼리가 느려짐
    • 각 인덱스를 탐색하고 새 항목을 삽입하기 위한 추가 I/O가 생김
    • 인덱스와 버퍼 풀 페이지 테이블 같은 내부 구조에서 lock/latch 경합이 생김
    • 실제 쿼리가 사용하지 않는 인덱스에도 유지보수 비용을 냄
    • Amazon Aurora처럼 IOPS 기준 과금이 있는 DBMS에서는 추가 읽기와 쓰기가 문제가 됨
  • OtterTune 고객 PostgreSQL 데이터베이스 분석에서 평균적으로 약 46%의 업데이트가 HOT 최적화를 사용함
    • 나머지 50% 이상 업데이트는 인덱스 유지보수 비용을 부담함
  • Uber의 2016년 Postgres에서 MySQL로 전환한 글은 이 문제를 보여주는 대표 사례임
    • 많은 보조 인덱스가 있는 테이블에서 write-heavy 워크로드가 큰 성능 문제를 겪었음
  • Oracle과 MySQL은 보조 인덱스가 새 버전의 물리 주소를 저장하지 않기 때문에 같은 문제가 없음
    • 보조 인덱스에는 튜플 ID나 기본 키 같은 논리 식별자를 저장함
    • DBMS는 이 논리 식별자를 사용해 현재 버전의 물리 주소를 찾음
    • 보조 인덱스 읽기는 느려질 수 있지만, MVCC 구현의 다른 이점으로 오버헤드를 줄임

문제 4: autovacuum 운영 난이도

  • PostgreSQL 성능은 obsolete 데이터를 제거하고 공간을 재사용하게 만드는 autovacuum 효과에 크게 의존함
  • RDS, Aurora, Aurora Serverless 모두 PostgreSQL 변형이므로 같은 autovacuum 문제를 가짐
  • autovacuum은 복잡해서 최적 상태로 돌리기 어려움
    • 기본 설정이 모든 테이블에 적합하지 않음
    • 특히 큰 테이블에서 문제가 커짐
  • autovacuum_vacuum_scale_factor의 기본값은 20%임
    • 1억 튜플 테이블에서는 최소 2천만 튜플이 갱신되어야 autovacuum이 트리거됨
    • 이 때문에 많은 dead tuple이 오랫동안 테이블에 남아 I/O와 메모리 비용을 유발할 수 있음
  • long-running transaction은 autovacuum을 막을 수 있음
    • expired version 정리가 늦어지면 dead tuple과 오래된 통계가 쌓임
    • 성능 문제가 long-running transaction을 더 만들고, 그 트랜잭션이 다시 autovacuum을 막는 악순환이 생길 수 있음
    • 이런 경우 사람이 long-running transaction을 수동으로 종료해야 할 수 있음

OtterTune 고객 사례

  • 한 PostgreSQL Amazon RDS 데이터베이스에서는 dead tuple 수가 2주 동안 톱니 모양으로 변했음
    • autovacuum이 하루에 한 번 정도 큰 정리를 수행함
    • 2월 14일에는 DBMS가 dead tuple 320만 개를 정리함
    • 그래프 전체에서는 dead tuple 수가 증가 추세였고, autovacuum이 따라잡지 못하는 비정상 상태였음
  • OtterTune 고객의 한 PostgreSQL RDS 인스턴스에서는 bulk insertion 이후 오래된 통계 때문에 long-running query가 발생함
    • 이 쿼리가 autovacuum의 통계 갱신을 막음
    • 그 결과 long-running query가 더 발생함
    • OtterTune의 자동 health check가 문제를 식별했지만, 관리자는 쿼리를 수동으로 종료하고 bulk insertion 이후 ANALYZE를 실행해야 했음
    • 해당 long query의 실행 시간은 52분에서 34초로 줄어듦

실무적 결론

  • DBMS 설계에는 항상 어려운 선택이 있고, 선택에 따라 워크로드별 성능이 달라짐
  • Uber의 특정 write-intensive 워크로드에서는 PostgreSQL의 MVCC로 인한 인덱스 쓰기 증폭이 MySQL 전환 이유였음
  • PostgreSQL의 MVCC 구현은 새 MVCC DBMS를 만든다면 따르지 말아야 할 방식으로 평가됨
    • append-only 저장과 autovacuum 조합이 핵심 문제임
    • 이 설계는 1980년대의 유산이며, 1990년대 이후 log-structured 시스템 패턴이 널리 퍼지기 전의 방식임
  • PostgreSQL은 여전히 선호되는 DBMS이지만, MVCC의 약점을 감수하고 운영해야 함
  • 우회 방법은 직접 많은 시간과 노력을 들여 튜닝하는 것임

댓글과 토론

Hacker News 의견들
  • Postgres 내부를 꽤 안다고 생각했는데도, 이 글은 훌륭했고 많이 배웠음
    근본적인 약점 중 하나는 Postgres가 행 버전 추적에 N2O가 아니라 O2N 방식을 택했다는 점으로 보임. N2O로 바꾼다고 모든 문제가 해결되진 않겠지만, 예컨대 전체 행 복사본을 저장하는 문제는 남겠지만, 80/20 관점에서는 현재 구현의 단점을 대부분 줄일 수 있을 듯함
    대부분의 트랜잭션은 최신 행 버전을 원할 테니, N2O 순서를 쓰면 오래된 버전이 필요할 때만 연결 리스트를 따라가면 되고, 각 행 버전을 인덱스에 저장하지 않아도 될 가능성이 큼

    • Andy의 YouTube 강의 History of Databases(CMU Databases / Spring 2020)를 보면 좋음
      첫 수업 전체를 호텔에 못 들어가서 암스테르담 거리에서 진행하는데, 캐릭터도 흥미롭고 내부 동작을 설명하는 능력이 엄청남
  • 큰 장점은 작업 부하가 주로 INSERT와 이후 테이블 삭제로 구성될 때 추가 공간이 필요 없다는 것
    일반적으로 삽입 트랜잭션을 쪼갤 필요도 없음. 생성 데이터 크기나 변경된 총 행 수에 실질적인 제한이 없기 때문임. 트랜잭션 안의 문장 수 제한은 있지만, 테이블을 너무 자주 바꾸지 않는다면 COPY FROM으로 우회 가능함
    DBA 관점에서는 롤백/언두 공간을 테이블 저장소와 별도로 관리할 필요가 없음. 애플리케이션마다 다르지만 PostgreSQL 설계가 모든 면에서 패배자는 아님. 버블 정렬 같은 건 아니라는 뜻임

    • 2000년대 초 게임 개발을 하면서 버블 정렬도 모든 면에서 패배자는 아니라는 걸 배웠음
      목록이 거의 정렬된 상태일 때 성능이 좋고, 3D 렌더링에서 카메라와의 거리 기준으로 객체를 정렬할 때가 그런 경우임. 카메라를 조금 움직이거나 회전하면 이전 프레임의 순서를 바탕으로 다시 정렬할 때 버블 정렬이 매우 잘 동작함
      최악의 경우를 피하려면 마지막 패스에서 실패한 비교 횟수와 지금까지 수행한 패스 수를 세다가 임계값을 넘으면 다른 정렬 알고리즘으로 전환하면 됨
    • 버블 정렬은 하드웨어나 거의 정렬된 집합에서는 훌륭함
    • 글에서 말 그대로 pg의 MVCC 설계가 90년대식이고 이제 아무도 그렇게 하지 않는다고 함
      30년 넘게 낡은 기술이니 모든 면에서 패배자는 아니더라도, 가장 중요한 측면에서는 패배자라고 봄
  • 이 글은 특히 다음 부분이 틀렸다고 봄
    “2000년대에는 Google과 Facebook 같은 떠오르는 기술 스타들이 MySQL을 쓴다는 이유로 통념이 MySQL을 선택했다. 2010년대에는 비내구성 쓰기 덕분에 ‘웹스케일’이 된 MongoDB였다. 지난 5년 동안 PostgreSQL은 인터넷이 사랑하는 DBMS가 됐다. 그럴 만한 이유가 있다! 신뢰성 있고, 기능이 풍부하며, 확장 가능하고, 대부분의 운영 작업 부하에 잘 맞는다.”
    똑똑한 엔지니어들이 Postgres를 고른 건 대중성에 호소하는 오류 때문이 아니라 데이터 안전성, ACID, Oracle과의 유사성, MVCC, SQL 표준 준수, Postgres 팀, 훌륭하고 도움 되는 커뮤니티, 데이터 타입, 높은 성능, BSD의 유연성 때문이었음
    2000년대 초 ATT에서 Postgres를 선택한 이유도 이것들이었고, Oracle DBA도 전환을 매우 쉽게 받아들였음. MySQL이 거친 전환을 겪는 동안 PG는 계속 강해지고 개선되어 왔음. Bruce Momjian이 이 성공에 큰 역할을 했다고 생각하며, 정말 뛰어난 커뮤니티가 있음

    • 비슷하게, 2005년에 MySQL에서 PostgreSQL로 선호가 바뀌었음
      오래된 AS400 데이터베이스 스키마와 현대적인 Rails 앱 사이에 “라이브” 호환 계층을 만들려고 데이터베이스 뷰를 쓰고 싶었기 때문임
      이후 데이터 안전성, 트랜잭션 안의 DDL 같은 것들 덕분에 선호가 계속 커졌음
  • “MySQL과 Oracle은 새 버전과 현재 버전 사이의 압축된 델타를 저장한다(git diff처럼 생각하면 된다).”
    Git은 유명하게도 diff를 저장하지 않고, 여기서 Postgres가 쓰는 방식처럼 새 객체와 이전 객체 전체를 저장하지 않나?

    • 인용한 문장은 Git이 diff를 저장한다고 말한 게 아니라, MySQL과 Oracle이 저장하는 것이 git diff와 비슷하다고 한 것임
    • 맞음. 파일의 각 버전은 별도의 블롭
      클론을 빠르게 하기 위해 패킹으로 압축을 하긴 하지만, Git이 다루는 원시 형태는 이런 블롭들임
    • 비교 대상은 Git 저장 방식이 아니라, MySQL과 Oracle 저장 방식이 git diff 형식과 비슷하다는 비유였음
      Git 저장소도 압축을 하고, 그 압축은 일종의 diff 기반이지만, 순진하게 예상하듯 커밋 이력 기반은 아님
    • 다른 사람들이 “git diffs”라고 했다는 점을 말했지만, Git은 실제로 pack file에서 저수준 최적화로 델타를 사용하며 MySQL 비교와 비슷함
      SQL 질의에서도 diff가 그대로 반환되는 건 아님
    • Git diff는 즉석에서 생성되지만, diff는 여전히 diff임
  • “PostgreSQL이 각 업데이트마다 테이블의 모든 인덱스를 수정해야 하는 필요성은 여러 성능상 영향을 낳는다. 당연히 시스템이 더 많은 일을 해야 하므로 update 질의가 느려진다.”
    쓰기 증폭에 대해 궁금했음. MySQL은 그런 식으로 인덱스를 업데이트할 필요가 없는 건 맞지만, MySQL 복제는 binlog에 의존하고, 모든 변경이 데이터베이스 자체(InnoDB redo log 등)에 더해 추가로 기록되어야 함
    그래서 클러스터에서 쓰는 MySQL은 다른 종류의 쓰기 증폭을 갖는 것처럼 보임. PostgreSQL은 복제에 WAL을 재사용하므로 그런 증폭은 없음
    게다가 수신 측에서 MySQL은 들어오는 binlog를 먼저 relay log에 쓰고, relay log를 applier 스레드가 소비하면서 추가 InnoDB 쓰기와 기본 설정상 추가 binlog를 만듦

  • 이 주제는 디스크를 빼고는 논의할 수 없음
    SSD는 한 번에 4KB 페이지를 씀. 즉 1비트만 업데이트해도 디스크는 4KB를 읽고, 비트를 바꾼 뒤 새 슬롯에 4KB 페이지를 다시 씀. 따라서 복사의 페널티는 디스크 종류에 따라 달라짐

    • 흥미롭다. 이게 AWS 과금에는 어떻게 반영되는지 궁금함
      AWS는 IO의 MBps에 대해 정액으로 과금하는데, 가장 가까운 4KB로 올림하는 규칙이 있는지, 아니면 요청량이 아니라 드라이브 자체의 쓰기 볼륨을 추적해서 저장소 구현에서 실제 IO 양을 과금하는지 모르겠음
    • Postgres 페이지는 8KB라서 그 포인트는 별 의미 없음
  • “Oracle과 MySQL은 MVCC 구현에서 이 문제가 없다. 보조 인덱스가 새 버전의 물리 주소를 저장하지 않기 때문이다. 대신 논리 식별자(예: 튜플 ID, 기본 키)를 저장하고, DBMS가 이를 사용해 현재 버전의 물리 주소를 찾는다. 이 때문에 보조 인덱스 읽기가 느려질 수는 있지만, 이 DBMS들은 오버헤드를 줄이는 MVCC 구현상의 다른 장점이 있다.”
    MySQL에서 관찰한 흥미로운 동작이 있음. 약 500GB 데이터베이스이고 스키마는 관계형보다는 문서 지향에 가까웠는데, SELECT id WHERE something; UPDATE what WHERE id=idUPDATE what WHERE something보다 몇 자릿수나 빠름
    이 동작의 이유가 아마 여기에 있지 않나 싶음. 다만 일반 작업 부하는 이렇게 하지 않고, 불일치를 고치는 임시 DML에서만 느려질 뿐임

    • SELECT는 읽기 전용 작업이라 병렬 수행이 가능함
      반면 UPDATE는 실제로 쓰기를 하고 테이블을 잠글 수도 있음. UPDATE id=id행 수준 잠금을 가능하게 함. 다만 SELECT와 UPDATE 사이에 새로 삽입된 레코드를 놓칠 위험도 있음
    • 읽기 위주의 2TB 초과 Postgres 인스턴스 몇 개를 운영하고 있고, 이것들도 문서 지향에 가까움
      대량 업데이트가 너무 느릴 수 있다는 데 동의함. 결국 업데이트를 배치 단위로 점진 처리하거나 심지어 COPY를 쓰는 경우가 많음
  • “2010년대에는 MongoDB였다. 비내구성 쓰기 덕분에 ‘웹스케일’이 됐기 때문이다.”
    주제에서 벗어나지만, 처음부터 끝까지 마케팅이었음: https://news.ycombinator.com/item?id=15124306

    • MongoDB는 전직 DoubleClick 엔지니어들이 다른 서비스에 필요한 요구사항을 만족하는 DB가 없어서 덤으로 만든 DIY DB로 설계됐음
      4.2.8 버전(2020)은 꽤 견고해졌고, 더티 쓰기가 없다고 알려져 있음: https://en.wikipedia.org/wiki/MongoDB#Technical_criticisms
  • 2024년에도 여전히 앞단에 연결 풀러(예: pgbouncer)를 둬야 쓸 만하다는 점이 가장 싫음

  • OrioleDB가 새 저장 엔진으로 이 문제를 해결하려고 했음: https://github.com/orioledb/orioledb

    • Supabase 팀에 있음
      Oriole은 이제 Supabase에 합류했고, Alexander와 그의 팀이 풀타임으로 작업 중임. 패치셋은 여기 있음: https://www.orioledb.com/docs#patch-set
      올해 말에는 Supabase 플랫폼에서도 사용해볼 수 있을 예정임