요약
스즈키 히로노부가 2015년부터 웹에 무료로 공개해 온 PostgreSQL 내부 구조 해설서입니다.
일본 PostgreSQL 사용자 그룹 이사를 지낸 개발자 스즈키 히로노부가 쓴 온라인 책입니다. PostgreSQL을 이루는 하위 시스템을 하나씩 풀어 전체 그림을 보여 주는 것이 목표입니다. 지금은 19 베타 버전까지 다룹니다.
왜 중요한가
- 데이터베이스 엔진을 직접 다루는 개발자들이 교재로 써 온 자료이고, 학술 논문에도 인용됐습니다.
- 10년 넘게 고쳐 쓰면서 비동기 I/O, 논리 복제 같은 최신 기능까지 따라가고 있습니다.
핵심 내용
- 프로세스·메모리 구조, 쿼리 처리, 동시성 제어, 버퍼 관리자, 복제 등 12개 장으로 이뤄져 있습니다.
- VACUUM(더는 쓰지 않는 옛 행을 치우는 작업)과 WAL(변경 내용을 먼저 적어 두는 로그)도 각각 한 장씩 다룹니다.
- HOT(인덱스를 고치지 않고 같은 페이지 안에서 행을 갱신하는 방식)와 인덱스 전용 스캔도 따로 설명합니다.
- 2026년 들어 논리 복제 장(베타)과 비동기 I/O 절을 새로 넣었습니다.
- 저작권은 저자에게 있지만 교육 기관에서는 본문과 그림을 자유롭게 쓸 수 있습니다.
HN 반응
- 여러 이용자가 실무에 가장 쓸모 있는 장으로 HOT를 꼽았고, 자주 바뀌는 열에 인덱스를 걸면 HOT가 꺼진다는 팁도 나왔습니다.
- 옛 행을 남겨 VACUUM으로 치우는 MVCC(다중 버전 동시성 제어) 설계를 두고, 오라클식 언두 로그가 낫다는 비판과 운영 문제라는 반론이 맞섰습니다.
관련 글:
The Internals of PostgreSQL - https://news.ycombinator.com/item?id=43241404 - 2025년 3월 (댓글 40개)
The Internals of PostgreSQL - https://news.ycombinator.com/item?id=30372750 - 2022년 2월 (댓글 19개)
The Internals of PostgreSQL - https://news.ycombinator.com/item?id=30086374 - 2022년 1월 (댓글 39개)
Internals of PostgreSQL - https://news.ycombinator.com/item?id=18950460 - 2019년 1월 (댓글 25개)
The internals of PostgreSQL - https://news.ycombinator.com/item?id=13488315 - 2017년 1월 (댓글 53개)
The Internals of PostgreSQL - https://news.ycombinator.com/item?id=12142364 - 2016년 7월 (댓글 1개)
(재게시는 1년쯤 지나면 괜찮습니다. 지난 글 링크는 궁금한 분들을 위한 것일 뿐입니다.)
particledb에 대한 최신 Jepsen 보고서를 보고 나서 postgres의 SSI 구현에 새삼 관심이 생겼습니다.
이 문서를 훑어만 보실 거라면 HOT 업데이트 경로 섹션부터 보시길 권합니다.
HOT를 이해한 다음 zHeap 구현 노트(TOAST 관련 내용도 포함)를 읽으면, postgres가 무엇을 하고 무엇을 하지 않는지에 대한 좋은 머릿속 모델이 잡힙니다.
참고로 2020년에 나온 Postgres 대상 Jepsen 보고서에서 SSI 구현의 오래된 버그가 발견됐습니다 [0]
[0] https://news.ycombinator.com/item?id=41560556
2011년부터 Safe Superintelligence 버그가 있었다고요?
와, 시대를 너무 앞서갔네요.
(제가 비꼬는 데 서툴러서 오해하시지 않게 덧붙이면, 여기서 SSI는 Serializable Snapshot Isolation입니다.)
본문에 정렬 패딩(alignment padding) 얘기가 안 보이길래 여기에 적어 둡니다. 테이블 컬럼 순서에 따라 레코드가 차지하는 공간이 달라진다는 걸 아셨나요?
그런데 이게 끝이 아닙니다. 행마다 약 30바이트의 오버헤드가 붙습니다. 18바이트짜리 행이 실제로는 54바이트를 차지하는 셈입니다. 게다가 얼마나 자주 업데이트하고 vacuum하느냐에 따라 그 몇 배가 되기도 합니다. 그렇다고 행을 너무 크게 만들어도 안 됩니다. TOAST로 넘어가거든요.
대부분의 시스템 프로그래밍 언어는 기본적으로 이렇게 동작하는데, postgres에서도 그렇다니 흥미롭네요. 어디였더라, zig였나 어떤 컴파일러였나, 필드 배치만 바꿔서 속도가 빨라지거나 최적화됐던 걸로 기억하는데 틀렸을 수도 있어요.
곁가지 이야기 주의: 제가 신경 쓰이는 점은, 디스크의 데이터를 그대로 네트워크로 흘려보내는 경우가 아니라면 데이터를 이런 식으로 직렬화해서 얻는 이점이 사실상 없다는 겁니다. (이걸 처음 깨달은 건 저장 효율을 높이려고 postgres에 varint와 byte 타입을 추가하려던 때였습니다. 그런데 공간을 실제로 절약하는 방식으로 직렬화하는 건 거의 불가능하다는 걸 알게 됐죠.)
런타임에 boolean을 할당했거나 어딘가에 바이트 하나를 저장하려 한다면, 그건 스택 아니면 힙에 있을 겁니다. 스택이라면 임시 값이고 적어도 한 번은 독립된 데이터 단위로 접근하게 되는데, 다른 작은 데이터와 묶어 두면 어차피 자기 레지스터로 꺼내려고 명령어가 하나 더 필요합니다. 그러니 더 촘촘하게 묶어서 얻는 이득은 사실상 없습니다. 힙이라면 메모리 사용량을 줄여서 실제로 얻는 게 있을 수도 있습니다. 하지만 데이터를 묶으면 같은 주소에 있는 모든 것의 수명과 구조가 한데 엮이고, 내부 구조를 확인하는 추가 조회가 필요하거나, 함께 묶인 데이터가 뭔지 몰라도 묶인 데이터를 꺼내는 명령어를 미리 박아 넣는 정교한 컴파일러·빌드 과정이 필요합니다.
타입이 있는 데이터베이스가 디스크에서 레코드의 직렬화된 데이터를 읽을 때는 레코드 안의 수명과 구조가 이미 공유되어 있습니다. 레코드를 불러와도 묶여 있는 특정 데이터에 접근하지 않는 경우가 대부분일 겁니다(예를 들어 테이블에 bool 컬럼이 5개 있어도 대부분의 쿼리는 그중 전부는 물론 하나도 쓰지 않습니다). 내부 타입과 최적화된 읽기를 처리하는 전역적인 방법은 이미 있습니다. 그게 데이터베이스가 하는 일이니까요.
그러니 묶인 데이터를 읽어 와서 효율적으로 접근하거나 계산하는 건 충분히 가능합니다. 예를 들어 bool을 쓰려면 워드에서 해당 bool 인덱스에 맞는 비트 하나만 뒤집힌 0x00..00 값과 XOR하면 되고, 서브워드를 레지스터에 올리려면 비트 시프트를 하면 됩니다. 효율적으로 구현하면 읽기당 명령어가 많아야 하나 더 느린 수준입니다(타입 조회와 최적화된 읽기 방식 결정은 쿼리 플래너가 맡을 수 있어서 비용이 분산되고, 쿼리의 병목도 아니고 크리티컬 패스에 있지도 않습니다).
하지만 데이터가 촘촘할수록 캐시 일관성이 좋아지고 읽기 처리량(초당 행 수)이 높아집니다. 게다가 묶인 데이터에서 값을 꺼내 계산하려고 비트 연산과 산술을 쓴다면 오히려 SIMD로 일괄 처리하기가 더 쉬울 수도 있습니다. 그러니 데이터를 더 촘촘하게 묶으면 성능은 더 높아지고 유휴 디스크 사용량은 크게 줄어(실제 돈이 절약됩니다) 아마 이득일 겁니다. 저는 직접 해 보자는 쪽으로 30% 정도 마음이 기울었습니다.
새로운 MVCC 얘기는 여기 없네요.
Postgres가 아직 그걸 채택하지 않았으니까요. 채택할 실제 계획이 있다는 것도 저는 모르고요. Tom Lane이 얼마 전 인터뷰에서 dead tuple과 vacuum에서 벗어나는 세상은 상상이 안 된다고 했습니다.
관심 있는 분들을 위해 Tom Lane 인터뷰를 올립니다: https://www.snowflake.com/en/blog/engineering/30-years-of-postgres-architecture-tom-lane/
MVCC에 관한 이 글도 꽤 흥미롭습니다: https://boringsql.com/posts/mvcc-bad-bad/
MVCC 글은 좋은 글이지만 "한편으로는 이렇고 다른 한편으로는 저렇다" 식의 분석을 너무 애써서 하려는 느낌이 있습니다. Postgres가 이 설계를 쓰는 유일한 데이터베이스인 이유는, 글이 끝에서 인정하듯 "운영상 시끄러운 장애 양상"이 있기 때문입니다.
데이터베이스 자원이 부족할 때 어떻게 되는지 Postgres와 Oracle(블록 undo 기반)을 비교해 보세요.
• Postgres: CPU가 병목이 됩니다. 데이터베이스가 vacuum을 따라가지 못하고 죽습니다.
• Oracle: 남은 디스크 공간이 병목이 됩니다. 오래 실행되는 개별 문장, SNAPSHOT 및 READ ONLY 트랜잭션이 "snapshot too old" 오류와 함께 취소됩니다.
Oracle에서는 데이터베이스가 계속 돌아가고 피해 범위도 문장 하나로 한정됩니다. 가장 오래 실행되는 문장부터 죽기 때문에, 대개는 실시간 사용자 트래픽이 아니라 비동기 업무 보고서가 죽는다고 보면 됩니다. 바로 바라던 결과죠. 대부분의 기업은 고객이 겪는 장애보다 보고서가 하루 비는 쪽을 훨씬 잘 견디고, 여유 디스크 공간만 있으면 고치는 것도 SQL 문장 하나면 됩니다(SAN 덕분에 저장 공간 추가도 쉽습니다).
이쪽이 훨씬 낫습니다. 어떤 스타트업이 Postgres의 MVCC 선택 때문에 몇 시간짜리 장애를 겪었다며 한탄하는 사후 분석(post mortem)을 HN에서 얼마나 자주 보셨습니까? 꽤 많죠. 반대로 대형 은행, 병원, 항만 같은 곳의 장애 사후 분석 가운데 원인이 Oracle의 MVCC 엔진으로 거슬러 올라가는 글을 읽은 기억이 있으십니까? 저는 한 번도 본 적이 없는 것 같습니다. 스타트업 업계는 대기업을 놀리길 좋아하지만, 대기업은 vacuum 기반 MVCC를 멀리하기 때문에 데이터베이스 가동 시간 실적이 훨씬 좋습니다.
공개: 저는 Oracle DB 그룹에서 시간제로 일하고 있습니다. 위에 승인받지 않은 의견이 있을 수 있으니 읽는 분이 알아서 판단하시기 바랍니다.
Postgres 방식의 MVCC(https://www.cs.cmu.edu/~pavlo/blog/2023/04/the-part-of-postgresql-we-hate-the-most.html 에서 말하는 "append-only storage")는 클라이언트에 객체 주소를 실제로 반환해서 포인터가 안정적이어야 한다면 필수입니다(저는 그런 DB를 구현해 본 적이 있습니다). 하지만 Postgres에는 사실 그런 요구 사항이 없으니, 그런 맥락에서는 이 방식이 별 의미가 없습니다. undo 로그 기반 MVCC의 장점은 커밋된 트랜잭션이 대체한 모든 버전이 같은 시점에 같은 연속된 메모리나 디스크 영역에서 해제된다는 점이고, 덕분에 메모리와 SSD 모두에 친화적입니다(https://research.cs.wisc.edu/wind/Publications/nvmw18-he.pdf 에서 말하는 "grouping by death time").
맞아요. postgres 내부를 공부하면서 저를 가장 헷갈리게 한 부분이 바로 이겁니다. append-only 방식이 안정적인 주소 지정을 가능하게 한다는 건 분명한데, 그게 실제로는 전혀 구현되지 않아요. 정말 당혹스러웠습니다. 내가 뭔가 놓치고 있는 거겠지 싶었는데, 아니었어요!
Postgres 쪽 분들이 바로잡아 주셔야 하겠지만, append-only MVCC 설계는 원래의 "타임 트래블" 요구 사항에서 나온 것 같습니다(그 요구 사항은 곧 폐기된 걸로 기억합니다). 어쨌든 타임 트래블에는 time-split B-tree 같은 더 나은 접근법이 있습니다( https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p939-lomet.pdf ).
타임 트래블은 어떤 MVCC 설계로도 할 수 있습니다. undo 로깅은 이 용도에 잘 맞고, Oracle은 Flashback이라는 이름으로 여러 형태의 타임 트래블 SQL을 제공합니다.
가장 단순한 형태의 Flashback은 undo 로그만 씁니다. 보통 undo는 그것을 쓰는 활성 트랜잭션이 없어지면 삭제되지만, 보존 시간을 설정하면 SELECT ... AS OF ... 쿼리로 최소한 그 시간만큼은 과거로 돌아갈 수 있습니다.
하지만 undo 로그를 영원히 보관하는 건 현실적이지 않습니다. 1년 전으로 돌아가고 싶다면 Flashback Time Travel이 있습니다. 이 기능은 쿼리 엔진이 AS OF 쿼리에 더 효율적으로 답할 수 있는 형태로 옛 데이터를 테이블에 복사해 둡니다.
time-split B-tree는 타임 트래블 쿼리를 시간과 공간 모두에서 효율적으로 처리하도록 설계된 구조입니다. 사소한 문제가 아니죠.
Flashback이 이 기법을 쓰는지는 모르겠지만, 저에게 time-split B-tree를 가르쳐 준 SQL Server 아키텍트가 지금은 Oracle에서 일하고 있습니다 :)
네, 그게 합리적이고 제가 기대했을 모습이기도 합니다. 그게 제공됐다면요. 그 설계가 시스템 전체를 떠받치고 있다는 건 이해하지만, 그래도 다시 검토된 적이 없다는 건 놀랍습니다. 실현 가능성이 충분했는데도 타임 트래블이 끝내 출시되지 않았다는 게, 적어도 저에게는 놀랍습니다.
감사합니다, MVCC 글 정말 재미있게 읽었어요.
네, 저도 그런 타임 트래블러들의 블로그가 좋아요. 다만 Oracle DB의 문제 말고 내년 시장 움직임을 알려 줬으면 좋겠네요.
boringsql 글은 우리 모두가 아는 그 공저자 특유의 수사만 눈감아 줄 수 있다면 아주 유익했습니다:
Claude, 이 글에서 Claude 티 나는 표현을 전부 빼 주세요.
이거 참 안타깝네요. mySQL 개발이 지금처럼 정체돼 있지만 않았어도 좋았을 텐데요.
소프트웨어 사용자들은 업스트림 오픈소스 프로젝트가 하는 대로 무조건 따라 하거나 따라 하지 않거나 하지는 않습니다.
OrioleDB는 SSD에 더 우호적이고, 사실상 Postgres의 "또 다른" MVCC입니다. OrioleDB가 아직 "베타"일 수는 있지만, 그렇다고 운영자와 사용자들이 쓰는 걸 막지는 못했습니다.
MariaDB에는 대안 MVCC가 있고, 그중 하나는 특히 뛰어납니다.
대규모 환경의 SSD 수명은 제게 계속 주요 걱정거리였기 때문에 그 마음에 공감합니다. 다른 분들도 OrioleDB를 알아야 하고, Postgres 전체로 봐도 그편이 아마 더 나을 겁니다.
Uber가 겪은 문제는 전부 그때 메일링 리스트에 도움을 청했다면 해결할 수 있었을 겁니다. 그 논의에 대한 반응으로 그중 몇 가지는 Postgres 자체에서 이미 수정되거나 바뀌기도 했고요.
애초에 "PostgreSQL"에서 "MySQL"로 옮겼다고 말하기도 어렵습니다. 새 시스템 밑에서 MySQL을 돌리는 건 맞지만, 사실상 스토리지 엔진만 쓰는 아주 이상한 하이브리드예요. 글에서 핵심 쟁점이던 보조 인덱스조차 데이터베이스 위에 얹은 별도 시스템이 관리합니다.
가능하다면 자세히 설명해 주세요.
Uber가 거기에 올린 글 덕분에 Postgres의 옛 MVCC, OrioleDB, MariaDB의 대안 MVCC를 조사해 보게 됐습니다.
제가 읽은 바로는 결국 행 하나를 바꾸면 행 전체가 다시 기록됩니다. 이게 정확하지 않은가요?
MariaDB의 MVCC는 행 전체를 다시 쓰지 않고 변경분(delta)만 씁니다. MariaDB의 더 새롭고 발전된 MVCC 중에는 효율이 더 높은 것도 있습니다.
제 생각에 "vacuum 폭주"는 언제나 IT 운영을 제대로 못 했거나, 사내 애플리케이션 개발자들이 데이터베이스 사용 패턴을 제대로 고민하지 않았거나(혹은 아예 고민하지 않았거나) 둘 중 하나였습니다. "하드웨어로 해결한다"가 좋은 답은 아니지만, ZFS ZiL/SLOG용 NVDIMM이 "vacuum 폭주"를 완화하지 못한 경우를 본 적이 없습니다. 보기 좋은 해법은 아니지만요. 물론 아주 오래전 일입니다.
고도화된 호스팅 환경에서는 SSD 수명이 부담스러운 수준이 될 수 있습니다. Prometheus+Grafana에 이런 지표를 갖춘 곳을 별로 못 봤다는 게 이상합니다. OrioleDB가 수명 걱정을 덜어 줬다고 말하는 분들은 자기 Grafana를 보여 줄 수 있는 처지가 아니고요.
앞으로 몇 년 안에 의미 있는 규모에서 SSD 수명 지표를 실제로 비교할 기회가 생길 수도 있겠지만, 확신은 없습니다.
그 밖에는, 실제 운영 중인 Postgres 데이터베이스의 스냅샷을 떠서 논리 복제로 1년 치 트래픽을 재생해 보고 SSD 수명에 미치는 영향의 차이를 확인하는 것이 유일한 방법이라고 생각합니다.
저는 이 사이트에 자주 오시는 최상위 전문가 분들에 비하면 Postgres 전문가가 아닙니다. Postgres에 대한 통찰은 언제든 환영합니다. 제 관심사는 대체로 웹 스케일 스택에서 그 부분의 북쪽이나 남쪽에 있었습니다.
2018년에 Greenplum에서 일할 때 저희 팀은 Andy Pavlo의 CMU 데이터베이스 강의와 함께 The Internals of PostgreSQL을 공부했습니다. 데이터베이스 커널을 다루는 사람에게는 더없이 값진 자료였습니다. 지금은 초보자도 AI와 대화하는 것만으로 그때 저희 팀이 나눴던 토론과 같은 대화형 학습 경험을 얻을 수 있습니다.
지금 저는 PostgreSQL용 새 캐시 서비스 Lavik을 만들고 있습니다. Redis 같은 기존 캐시는 DRAM 기반이라 자주 쓰는 데이터의 작은 일부만 담을 수 있습니다. 데이터가 메모리를 넘어서면 캐시 미스 폭풍에 노출됩니다. 요청이 한꺼번에 PostgreSQL로 쏟아지는 상황이죠. 이런 걸 좋아하는 사람은 없습니다. (OpenAI도 비슷한 캐시 미스 폭풍 문제를 겪었습니다: https://openai.com/index/scaling-postgresql/ )
Lavik은 다른 길을 갑니다. NVMe SSD 위에서 돌아가는데도 DRAM에서 돌아가는 Redis보다 빠릅니다. 대부분의 사람이 예상하는 바와 다릅니다. 통념은 SSD 기반 시스템은 속도를 내주고 용량을 얻는다고 말하지만, Lavik은 둘 다 제공합니다. TB 규모의 데이터를 캐시에 두면서도 1M QPS와 1밀리초 미만의 P99 지연 시간을 얻을 수 있습니다.
Lavik은 Apache License로 공개된 오픈소스입니다. GitHub: https://github.com/eloqdata/lavik 웹사이트: https://lavik.dev
GET이랑 SET(그리고 비슷한 것들)은 쉽죠. RMW 연산이나 여러 키 명령을 다루는 트랜잭션, 클러스터링 지원까지 필요해지면 똑같은 성능이 나오는지 알려 주세요 :)
그리고 GET과 SET만 해서 Redis보다 빨라지는 건 꽤 쉬워요. cachegrand의 아주아주아주 초기 버전(0.1 이전)은 32코어/64스레드 머신에서 파이프라이닝을 켜고 GET과 SET을 초당 1억 건이 훨씬 넘게 처리했거든요.
나머지를 덕지덕지 붙여서 실제로 "쓸 만하게" 만들기 시작하면 그때부터 성능이 크게 떨어지는 거예요.
12장(전문 검색)은 Postgres 18에서 이 책 모르게 슬쩍 바뀌었습니다. FTS가 이제 사전과 설정을 항상 libc로 읽지 않고 클러스터의 기본 콜레이션 제공자를 통해 읽습니다. 그래서 ICU나 새 builtin PG_UNICODE_FAST를 쓰는 클러스터에서는 FTS와 pg_trgm의 동작이 달라질 수 있고, 릴리스 노트에도 pg_upgrade 후에 해당 인덱스를 다시 만들라고 분명히 적혀 있습니다. 또 18에서는 nondeterministic 콜레이션과 함께 쓴 LIKE가 더는 오류가 되지 않는데, 이게 검색 대상 컬럼에 대소문자 구분 없는 ICU 콜레이션을 쓰지 않으려 했던 주된 이유였습니다.
실무에서 바로 효과를 볼 수 있는 장은 HOT 장입니다. 거기서 알아 둘 만한 게 두 가지 있습니다. HOT는 인덱스가 걸린 컬럼이 하나도 바뀌지 않고 새 튜플이 같은 페이지에 들어갈 때만 작동하므로, 자주 갱신되는 컬럼(status, updated_at)에 인덱스를 걸면 그 테이블에 대한 모든 업데이트에서 HOT가 조용히 꺼집니다. 그리고 업데이트가 많은 테이블에서 fillfactor를 낮추면(80~90 정도) 각 페이지에 빈 공간이 남아서 HOT가 실제로 일어날 수 있습니다. 튜닝에 앞서 HOT 업데이트가 되고 있는지 가장 싸게 확인하는 방법은 pg_stat_user_tables에서 n_tup_hot_upd와 n_tup_upd를 비교해 보는 것입니다.