ClaudeがCRISPR様酵素を発見、Devinは10億ドル到達
AIタグが付けられた新着記事 - Qiita
Anthropic は 950 個の Claude エージェントを DNA データベースに向け、誰も名前を付けていなかった酵素システムを持ち帰った。Cognition は年換算売上レートが 10 億ドルを超えたと発表し、5 月の約 4 億 9,200 万ドルから 4 か月...
Балл: 57.4Уверенность: 54%
ПодробнееЗагружаем каталог…
НАВИГАТОР ПО ВОЗМОЖНОСТЯМ ИИ
Найдите свой ИИ-инструмент. Бесплатный доступ, пробные периоды и кредиты — в одном месте.
AIタグが付けられた新着記事 - Qiita
Anthropic は 950 個の Claude エージェントを DNA データベースに向け、誰も名前を付けていなかった酵素システムを持ち帰った。Cognition は年換算売上レートが 10 億ドルを超えたと発表し、5 月の約 4 億 9,200 万ドルから 4 か月...
Балл: 57.4Уверенность: 54%
ПодробнееAIタグが付けられた新着記事 - Qiita
はじめに TypeSafe AI の「Jev」を触っていて、真っ先に思いついたのがウミガメのスープでした。 Jev は文章を1文字も書きません。「はい/いいえ/関係ありません」のような選択肢を渡すと、それぞれの確率だけを返してきます。ウミガメのスープの出題者がやることも、...
Балл: 57.39Уверенность: 54%
ПодробнееAIタグが付けられた新着記事 - Qiita
AI より詳しくなくても、AI の回答を疑うことはできるか。AI がそれらしい答えをしてくると「そうなんだ~」で済ませてしまう問題、納得してしまう問題。全く分からない分野について知識で勝負するのは無理なのでどうすれば AI に対抗できるのか考えています。 結果 AI の答え...
Балл: 57.38Уверенность: 54%
ПодробнееAIタグが付けられた新着記事 - Qiita
本記事は筆者が運営する AI Quotidia の海外ニュース解説記事です。 NTTドコモのモバイル社会研究所が2026年9月24日(木)に公表した「生成AI利用意識・行動調査」で、生成AIの利用者が挙げた不満のうち「誤情報が含まれている」は、2025年2月の34.8%...
Балл: 57.38Уверенность: 54%
ПодробнееReadhub
第二十四届中国国际摩托车博览会期间,新能源摩托车及相关核心零部件成为海外采购商关注重点。当前两轮车出海逻辑发生变化,电动化产品契合南美、东南亚、中东等市场的多元需求。我国电动两轮车相关企业新增供给正围绕电池管理、电机控制等多环节展开,出口扩张将带动电芯、轻量化材料等产业链相关环节获得订单机会,同时海外认证、备件仓等也会增加企业出海成本,缺少合规认证和售后体系的中小厂商将面临更强约束。今年上半年重庆出口电动摩托车及脚踏车 11.7 万辆,同比增长 130.5%,出口金额 5.15 亿元,同比增长 66.6%。截至今年 9 月,我国现存摩托车相关企业 229.6 万余家,电动两轮车相关企业已突破 2100 家。
Балл: 57.15Уверенность: 54%
Подробнее掘金
最近公司在招聘,主要是社招5年以上经验的,我也是从候选人转变成了面试官,来记录下面试官的体验,也给还在面试的友友提供参考 简单的说,面试其实就是看感觉,觉得候选人行,那就过,不行就下一个。 让人挂挺容
Балл: 57.13Уверенность: 54%
Подробнееvelog
크래프톤! 배틀그라운드 만든 회사죠? 그런데 이 회사! 2026년 1분기에는 대규모 흑자, 2분기에 적자를 냈습니다! 어? 배그 그렇게 잘 나가는데 왜 2분기는 적자가 났을까? 오늘은 크래프톤이 뭐해서 돈을 버는 회사인지, 그리고 2분기 적자가 진짜 문제인지 한번 보겠습니다. 크래프톤 하면 역시! 배틀그라운드! PC, 콘솔, 모바일까지 전 세계에서 서비스하고 있습니다. 그런데 크래프톤이 배그만 하는 회사냐? 그건 아닙니다. 서브노티카, 인조이, 그리고 다양한 신규 게임 IP를 확보하면서 배그에서 번 돈으로 새로운 게임을 계속 만드는 회사 라고 보면 됩니다. 그런데! 여기서 중요한 숫자가 나옵니다. 2026년 2분기! 매출은 1조 2,902억 원! 영업이익은 4,109억 원! 전년 동기 대비 매출은 약 95%, 영업이익은 약 67% 증가했습니다. 그런데! 당기순이익은? 마이너스 299억 원! 적자입니다! 뭔가 이상하죠? 매출도 엄청 늘고, 영업이익도 4천억 넘게 벌었는데 왜 마지막에는 적자냐? 여기서 영업이익과 당기순이익의 차이를 봐야 합니다. 영업이익은 게임을 만들어서 팔고, 게임을 운영하면서 본업으로 얼마를 벌었는지를 보는 겁니다. 그런데 당기순이익은 여기에 금융손익, 기타손익, 세금 같은 것까지 전부 반영하고 마지막에 남는 돈입니다. 그리고 이번 크래프톤의 경우! 영업 외 손실이 크게 발생했습니다. 원인은 크래프톤이 인수한 언노운월즈! 서브노티카를 만든 회사입니다. 크래프톤은 이 회사를 인수하면서 경영진에게 조건부 성과보상, 이른바 언아웃을 약속했는데 이걸 두고 소송이 벌어졌고, 결국 합의하면서 2분기에 약 3,167억 원의 영업 외 손실이 발생했습니다. 그래서! 게임을 못 팔아서 적자가 난 게 아닙니다. 오히려 반대입니다. 본업은 엄청나게 잘하고 있습니다. 2026년 상반기 기준으로 매출은 2조 6,616억 원! 영업이익은 9,725억 원! 둘 다 상반기 기준 역대 최고입니다. 그리고 여기서 또 하나 재미있는 게 있습니다. 바로 서브노티카2! 2026년 5월 얼리 액세스로 출시했는데, 출시 22일 만에 전 세계 판매량 500만 장! 을 돌파했습니다. 그러니까 크래프톤 입장에서 배그가 돈을 벌어주고, 그 돈으로 새로운 IP를 만들고, 그중 하나가 서브노티카2처럼 터지는 구조가 만들어지고 있는 겁니다. 여기서 크래프톤의 핵심을 봐야 합니다. 배그가 계속 돈을 벌 수 있느냐? 그리고 배그 다음에 또 다른 게임을 만들어낼 수 있느냐? 이겁니다. 왜냐하면 현재 크래프톤의 가장 큰 약점도 결국 PUBG 의존도이기 때문입니다. 배그가 아무리 잘 나가도 배그 하나에 너무 의존한다면 장기적으로는 부담이 될 수 있습니다. 그래서 앞으로는 서브노티카2! 인조이! 그리고 앞으로 나올 신규 게임들이 얼마나 성공하는지를 봐야 합니다. 특히 서브노티카2가 단순히 한 번 잘 팔린 게임으로 끝나는지, 아니면 크래프톤의 새로운 글로벌 IP로 자리 잡는지! 이게 중요합니다. 정리하면! 크래프톤 2분기 적자? 게임사업이 망해서 난 적자가 아닙니다. 매출 1조 2,902억! 영업이익 4,109억! 본업은 오히려 크게 성장했습니다. 다만 언노운월즈 관련 일회성 성격의 영업 외 손실이 크게 발생하면서 최종적으로 당기순손실 299억 원이 나온 겁니다. 그래서 크래프톤을 볼 때는 “2분기 적자!” 이 숫자 하나만 볼 게 아니라, 앞으로 배그가 얼마나 오래 돈을 벌어주는지! 그리고 서브노티카2 같은 제2의 글로벌 IP를 계속 만들어낼 수 있는지! 이걸 봐야 합니다. 배그로 번 돈으로 다음 배그를 만드는 게 아니라, 배그 없이도 돈을 벌 수 있는 게임회사를 만들고 있는지! 이게 크래프톤의 진짜 관전 포인트입니다. 그럼 차트를 한번 보겠습니다. 주봉 RSI는 34, 일봉 RSI는 31까지 내려오면서 과매도권에 가까워진 모습입니다. 여기서 중요한 가격은 19만 8천 원! 이 가격을 지켜준다면 단기 반등하면서 22만 원, 24만 원까지 올라갈 가능성을 볼 수 있고, 24만 원을 돌파하면 다음은 28만~31만 원 구간이 중요한 저항입니다. 반대로 19만 8천 원이 깨진다면? 다음 지지선은 17만 2천 원 부근입니다. 결국 지금 크래프톤은 20만 원을 지켜내느냐, 깨느냐가 핵심! 주봉과 일봉 모두 아직 하락 추세이기 때문에 반등이 나오더라도 24만 원을 돌파하기 전까지는 추세 전환으로 보기는 어렵습니다. 2분기 적자 때문에 주가가 떨어진 것이라기보다는, "역대급 실적은 확인했는데, 그 실적이 앞으로도 지속될 수 있는지에 대한 시장의 의구심이 주가를 누르고 있는 것" 에 더 가깝습니다. 그리고 Unknown Worlds 소송·합의 비용은 단기 악재, PUBG 성장 지속성 + 신규 IP 성공 여부는 중장기 주가의 핵심 변수라고 보는 게 적절합니다. 차트까지 연결하면 더 재미있습니다. 지금 사용자님이 올려주신 차트에서 약 19만8천 원 부근까지 내려온 것도 이런 심리와 연결해서 볼 수 있습니다. 즉, 실적은 최고 → 그런데 주가는 하락 → 시장은 미래 성장률을 의심 → 차트상 19만8천 원 지지 테스트 이렇게 연결하면 됩니다. 유튜브 대본으로 만든다면 저는 “실적은 역대급인데 주가는 왜 계속 빠질까?”를 메인 주제로 잡는 게 훨씬 재미있다고 봅니다. 특히 소송 때문이라고 단순 결론을 내리기보다 일회성 소송 비용 vs 미래 성장률 둔화 우려를 대비시키는 방식이 좋습니다. 이상 골든 엉클이었습니다!
velog
이 글에서 다룰 주제 가시성: 동시에 접근하는 두 세션은 어떤 버전을 보는가? 저장 구조: 행 버전과 페이지의 주소는 어떻게 연결되는가? 업데이트 최적화: HOT·fillfactor·pruning은 어떤 비용을 줄이는가? 주요 단어 · MVCC · Snapshot · xmin/xmax · Heap Page · ctid · HOT · fillfactor · Pruning 테이블의 행 하나를 수정했는데 내부에는 같은 논리 행의 여러 버전이 남을 수 있다. 이는 불필요한 복사만을 뜻하지 않는다. 다른 트랜잭션이 자신의 스냅샷에 맞는 데이터를 읽기 위해 필요한 구조다. 이번에는 보이는 행과 물리적으로 저장된 버전을 구분한 뒤, HOT가 인덱스 유지 비용을 줄이는 방법까지 따라간다. MVCC 는 여러 행 버전과 스냅샷으로 가시성을 판단하는 방식이다. HOT 는 같은 페이지의 새 버전에 일반 인덱스 엔트리를 추가하는 일을 줄이는 UPDATE 최적화다. 자료와 예제 기준 — PostgreSQL 18을 중심으로 개인 학습 노트를 재구성했다. SQL·실행 계획·설정값은 설명 및 재현용 예제이며 이 글을 위해 운영 DB에서 새로 측정한 결과는 아니다. DDL/DML 예제는 독립적인 테스트 환경에서 사용한다. 1. 스냅샷과 다른 DB의 버전 관리 비교 트랜잭션과 MVCC 트랜잭션은 여러 변경을 함께 커밋하거나 롤백하는 경계이다. MVCC는 행 버전과 스냅샷을 이용해 동시 접근에서 어떤 변경을 볼 수 있는지 결정한다. 스냅샷은 데이터베이스 전체 복사본이 아니다. 트랜잭션 ID 범위와 진행 중 트랜잭션 등의 정보를 이용하는 가시성 판단 기준이다. 일반 SELECT와 UPDATE 사이의 행 잠금 경합은 줄지만, 같은 행의 동시 쓰기와 명시적 잠금은 여전히 대기하거나 충돌할 수 있다. PostgreSQL과 InnoDB의 가장 큰 차이 항목 PostgreSQL MySQL InnoDB 기본 행 저장 Heap 클러스터드 인덱스 리프 UPDATE 새 튜플 버전 생성 현재 레코드 수정 + Undo 기록 이전 버전 Heap의 이전 튜플 Undo로 이전 상태 재구성 가시성 메타데이터 xmin·xmax 등 DB_TRX_ID·DB_ROLL_PTR 등 정리 VACUUM·페이지 pruning Purge 긴 스냅샷의 영향 과거 튜플 회수 지연 필요한 Undo 회수 지연 이는 대표적인 저장 원리를 단순화한 비교다. 기본 키 변경 등 모든 수정이 InnoDB에서 동일한 물리적 in-place 갱신으로 끝나는 것은 아니다. PostgreSQL 예시 트랜잭션 100이 pending 을 넣고 200이 completed 로 바꾼다고 가정한다. 튜플 status xmin xmax 이전 버전 pending 100 200 새 버전 completed 200 0 xmin: 이 행 버전을 만든 트랜잭션. xmax: 삭제·교체 또는 잠금 관련 정보를 표현하는 필드. 0이 아니라고 무조건 삭제된 행은 아니다. ctid: 튜플의 물리적 위치. UPDATE 등으로 바뀔 수 있으므로 업무 식별자로 쓰지 않는다. 가시성은 숫자 크기만으로 판단하지 않고 커밋 상태·스냅샷·자기 트랜잭션 변경 등을 고려한다. InnoDB 예시 현재 행의 DB_TRX_ID로 가시성을 판단하고 필요하면 DB_ROLL_PTR가 가리키는 Undo 정보를 적용해 이전 행을 재구성한다. Undo는 반드시 과거 행 전체의 복사본인 것은 아니다. InnoDB 보조 인덱스는 클러스터드 레코드와 다르게 관리된다. 인덱스 키 변경 시 이전 항목에 삭제 표시를 하고 새 항목을 넣는다. MVCC 확인 때문에 클러스터드 레코드와 Undo 접근이 추가로 필요할 수 있다. 기본 격리 수준도 다르다 격리 수준 PostgreSQL InnoDB 기본값 Read Committed Repeatable Read Read Committed 일반 조회 문장마다 새 스냅샷 consistent read마다 새 스냅샷 Repeatable Read 첫 비제어 문장 시점의 스냅샷 유지 첫 consistent read 시점의 Read View 유지 두 DB 모두 자기 트랜잭션의 변경은 볼 수 있다. InnoDB의 WITH CONSISTENT SNAPSHOT 같은 명시적 시작 방식은 별도 조건이 있다. 반복 조회 예시 순서 A B 1 BEGIN 2 SELECT → pending 3 UPDATE → completed, COMMIT 4 같은 SELECT 기본 설정에서 A의 두 번째 일반 SELECT는 PostgreSQL에서는 completed, InnoDB에서는 pending을 볼 수 있다. PostgreSQL도 RR로 바꾸면 이 예제에서는 pending을 유지한다. 이는 저장 구조 자체보다 격리 수준의 차이이다. RR의 쓰기 동작까지 같지는 않다. PostgreSQL에서는 스냅샷 이후 다른 트랜잭션이 수정·커밋한 행을 갱신하려다 serialization failure가 발생할 수 있다. InnoDB의 잠금 조회·UPDATE는 일반 consistent read와 다른 방식으로 최신 상태를 대상으로 동작할 수 있고, 조건에 따라 next-key lock을 사용한다. 2. 같은 행, 서로 다른 버전 학습 자료의 개념도 — 행 버전과 스냅샷. 세부 조건은 본문 설명을 함께 읽는다. 예시는 성공한 UPDATE와 서로 다른 스냅샷을 단순화한 것이다. 행 버전 progress xmin xmax 예시 가시성 V1 10 100 200 업데이트 전 스냅샷에서 보일 수 있음 V2 50 200 0 트랜잭션 200의 커밋을 보는 스냅샷에서 보일 수 있음 트랜잭션 200이 성공적으로 UPDATE한 단순 예시다. 실제 가시성은 커밋 여부, 스냅샷, 명령 순서 등을 함께 판단한다. xmax != 0 은 무조건 삭제되었다는 뜻이 아니다. 행 잠금에도 사용될 수 있다. xmin / xmax — xmin은 생성 트랜잭션, xmax는 삭제·대체 또는 잠금 관련 정보다. 숫자만으로 가시성을 판단하지 않는다. 격리 수준 — READ COMMITTED는 문장마다 스냅샷을 사용한다. REPEATABLE READ는 트랜잭션 스냅샷을 유지한다. 읽기와 쓰기 — 일반 SELECT와 UPDATE의 충돌을 줄인다. 같은 행을 수정하는 트랜잭션끼리는 기다릴 수 있다. 기억할 문장: 오래된 버전 ≠ 즉시 제거 가능한 버전. 가시성 판단이 먼저다. 3. 8KB 페이지 안의 주소록 Heap 슬롯(line pointer)과 실제 tuple 데이터는 서로 다른 공간이다. 페이지 구성 역할 Page Header 일반적으로 24바이트. LSN·빈 공간 경계 등 ItemIdData / line pointer 슬롯당 4바이트. 위치·길이·상태 Free Space 슬롯과 tuple을 배치할 여유 공간 Tuple 데이터 행 헤더와 컬럼 값 인덱스의 TID (10,1) 은 페이지 10의 슬롯 1을 뜻한다. 해당 슬롯이 실제 tuple의 바이트 위치를 가리킨다. ctid 는 현재 행 버전의 물리 위치를 나타내는 시스템 컬럼이다. t_ctid 는 tuple 헤더에 있는 자기 자신 또는 후속 버전의 위치 정보로 구분한다. 슬롯 — 위치·길이·상태를 담는다. 인덱스는 페이지 번호와 슬롯 번호로 heap에 접근한다. 헤더 — tuple에는 xmin·xmax·t_ctid·infomask 등이 있다. HOT와 HHU는 tuple 헤더 플래그다. 주소 재사용 — ctid는 UPDATE나 재작성으로 바뀌고 슬롯도 재사용된다. 업무상 식별에는 PK를 사용한다. 기억할 문장: 슬롯은 주소록, tuple은 데이터. 둘의 수명은 같지 않을 수 있다. 4. fillfactor는 UPDATE의 여유 공간 INSERT 때 얼마나 채울지 정한다. 페이지 점유율의 영구적인 상한이 아니다. 설정 INSERT 시 목표 UPDATE에 미치는 영향 fillfactor=100 페이지를 최대한 채움 같은 페이지 여유가 부족할 수 있음 fillfactor=80 대략 20%를 여유로 남김 새 버전을 같은 페이지에 배치할 기회 증가 HOT는 새 버전이 같은 페이지에 들어가고 일반 인덱스가 참조하는 컬럼의 값이 변경되지 않는 조건에서 가능하다. BRIN 같은 요약 인덱스는 예외가 있다. 표현식 인덱스의 입력 컬럼, 부분 인덱스 조건, INCLUDE 컬럼도 검토해야 한다. UPDATE가 여유 공간을 사용하므로 실제 점유율이 fillfactor를 넘을 수 있다. HOT의 두 조건 — 페이지 공간뿐 아니라 인덱스 참조 컬럼의 변경 여부도 맞아야 한다. BRIN 등은 별도 예외가 있다. 공간과 성능의 교환 — 낮은 fillfactor는 UPDATE에 유리할 수 있지만, 초기 페이지 수와 읽기·캐시 비용이 늘어난다. 기존 데이터 — ALTER TABLE ... SET (fillfactor=80)만으로 기존 페이지가 즉시 재배치되거나 bloat가 사라지지 않는다. 기억할 문장: 여유 공간 확보 + 인덱스 조건 충족 → HOT 가능성 증가. 5. 인덱스 하나로 버전을 따라간다 학습 자료의 개념도 — HOT chain과 tuple 플래그. 세부 조건은 본문 설명을 함께 읽는다. 모두 커밋되었고 pruning 전인 HOT chain의 단순화된 예시 같은 heap 페이지에서 두 번 HOT 업데이트가 성공했고 아직 pruning 전인 예시다. 플래그 비트 값 의미 HEAP_HOT_UPDATED / HHU 0x4000 해당 버전에서 HOT 업데이트가 일어남 HEAP_ONLY_TUPLE / HOT 0x8000 별도 일반 인덱스 엔트리 없이 생성된 heap-only 버전 V1은 원래 인덱스 진입점이다. V2는 HOT로 생성되었고 다시 HOT 업데이트되었으므로 두 플래그가 모두 1이다. V3의 t_ctid는 이 예시에서 자기 자신을 가리킨다. 플래그만으로 커밋 여부나 가시성을 확정하지 않는다. Heap-Only Tuple — heap에는 새 버전을 만들지만 일반 인덱스의 새 엔트리를 생략한다. WAL·이전 버전은 여전히 생긴다. 가시성 — 인덱스에서 진입한 뒤 HOT chain에서 스냅샷에 보이는 버전을 찾는다. 무조건 최신 버전은 아니다. 중간 버전 — V2는 HOT로 생성되고 다시 HOT 업데이트되었으므로 HHU와 HOT가 모두 1일 수 있다. 기억할 문장: HHU는 후속 연결, HOT는 heap-only 버전이라는 표시다. 6. 데이터는 지워도 진입점은 남긴다 학습 자료의 개념도 — 페이지 슬롯의 네 상태. 세부 조건은 본문 설명을 함께 읽는다. V1·V2는 더 이상 필요 없고 V3는 살아 있다고 가정한다. 공간 Pruning 전 Pruning 후 인덱스 엔트리 슬롯 1 참조 그대로 유지 슬롯 1 NORMAL → V1 REDIRECT → 슬롯 3 슬롯 2 NORMAL → V2 UNUSED 슬롯 3 NORMAL → V3 그대로 유지 V1·V2 데이터 공간 점유 회수 V1·V2를 어떤 트랜잭션도 더 이상 필요로 하지 않는다고 가정한다. V3로 가는 경로가 필요한 동안 VACUUM도 인덱스 진입점과 redirect를 없애지 않는다. chain 전체가 회수 가능해지면 인덱스 참조와 root 슬롯까지 정리할 수 있다. 한 페이지 안에서 — pruning은 페이지 내부에서 불필요한 버전과 HOT 연결을 정리한다. 인덱스를 직접 청소하지 않는다. REDIRECT — 옛 tuple의 t_ctid가 아니다. V1 데이터는 없어지고 슬롯 자체가 필요한 후속 슬롯을 가리킨다. VACUUM과의 관계 — VACUUM도 pruning을 수행한다. 둘은 경쟁하는 대안이 아니라 범위가 다른 유지보수 작업이다. 기억할 문장: 인덱스를 수정하지 않고 페이지 안에서 재사용 가능한 공간을 만든다. 7. NORMAL은 “살아 있음”이 아니다 슬롯의 물리 상태와 tuple의 MVCC 가시성을 구분하자. 상태 담고 있는 의미 재사용 NORMAL 실제 tuple 데이터 위치·길이 아직 불가 REDIRECT 같은 페이지의 다른 슬롯 번호 불가 DEAD 유효 버전은 없지만 참조 정리가 필요한 슬롯 아직 불가 UNUSED 사용하지 않는 슬롯 가능 인덱스가 있는 일반 heap에서의 대표 경로다. 실제 구현은 인덱스 유무와 최적화에 따라 중간 단계가 합쳐질 수 있다. 슬롯 상태는 HHU/HOT tuple 헤더 플래그와 별개다. NORMAL — tuple 데이터가 있다는 뜻이다. 오래된 버전이나 삭제된 버전도 아직 NORMAL일 수 있다. DEAD — DELETE 직후 자동으로 바뀌지 않는다. 회수 가능해진 뒤 pruning 등의 단계에서 나타난다. UNUSED — 페이지 안에서 슬롯을 다시 쓸 수 있다는 뜻이다. 파일 공간을 OS에 돌려줬다는 뜻은 아니다. 기억할 문장: 슬롯 상태: 어디를 가리키나? / MVCC: 누구에게 보이나? 8. 두 세션으로 가시성 관찰하기 격리된 환경에서 해 보는 실습 1 MVCC·Rollback을 두 세션으로 관찰 아래는 교육용 DB에서 실행한다. ORM Flush가 전송한 UPDATE를 SQL로 직접 관찰하는 실습이다. CREATE TABLE study_orders ( id bigint PRIMARY KEY, status text NOT NULL ); INSERT INTO study_orders VALUES (42, 'PENDING'); 세션 A: BEGIN; UPDATE study_orders SET status = 'PAID' WHERE id = 42; SELECT id, status, xmin, xmax FROM study_orders WHERE id = 42; -- 자기 변경인 PAID를 확인. 아직 COMMIT하지 않는다. 세션 B: SELECT id, status, xmin, xmax FROM study_orders WHERE id = 42; -- 일반 SELECT는 미커밋 PAID가 아닌 PENDING을 조회한다. 세션 A: ROLLBACK; 세션 B: SELECT id, status, xmin, xmax FROM study_orders WHERE id = 42; -- PENDING. 시스템 컬럼 값만 보고 커밋·삭제를 단정하지 않는다. 2 오래된 스냅샷 확인 세션 B에서 먼저 실행: BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT status FROM study_orders WHERE id = 42; -- 이 SELECT로 스냅샷 확보 세션 A: BEGIN; UPDATE study_orders SET status = 'PAID' WHERE id = 42; COMMIT; 세션 B: SELECT status FROM study_orders WHERE id = 42; -- 기존 스냅샷: PENDING COMMIT; SELECT status FROM study_orders WHERE id = 42; -- 새 스냅샷: PAID 일반 SELECT는 모든 물리 버전을 나열하지 않는다. Tuple header의 원시 상태나 WAL 자체를 살피려면 별도의 진단 도구와 권한이 필요하다. 자료 기준과 참고 문서 개인 PostgreSQL 학습 노트를 바탕으로 정리했다. 첨부 그림은 제공된 학습 자료를 사용했으며, 버전이나 설정에 따른 조건은 본문에 덧붙였다. 격리 수준 페이지 구조 HOT 시스템 컬럼 MySQL InnoDB MVCC 이어서 읽기 · ← 이전 편 · 다음 편 → · 전체 시리즈 목차
velog
이 글에서 다룰 주제 탐색 단위: 키를 찾는 것과 페이지 범위를 건너뛰는 것은 어떻게 다른가? 분할 단위: 파티셔닝은 인덱스와 어떻게 함께 쓰이는가? 검증: 데이터 순서와 실행 계획에서 무엇을 확인할 것인가? 주요 단어 · B-tree · BRIN · Correlation · Operator Class · Partition Pruning · Sharding 큰 테이블을 빠르게 읽으려면 먼저 어디에서 읽을 대상을 줄일지 정해야 한다. B-tree는 키를 따라가고, BRIN은 물리적 페이지 범위를 요약하며, 파티션 Pruning은 조건과 무관한 파티션을 제외한다. 세 가지는 서로 대체하는 제품 목록이 아니라 다른 크기의 탐색 단위를 다루는 도구다. BRIN 은 인접한 블록 범위를 요약하는 인덱스이고, Partition Pruning 은 조회 조건과 무관한 파티션을 실행 대상에서 제외하는 최적화다. 자료와 예제 기준 — PostgreSQL 18을 중심으로 개인 학습 노트를 재구성했다. SQL·실행 계획·설정값은 설명 및 재현용 예제이며 이 글을 위해 운영 DB에서 새로 측정한 결과는 아니다. DDL/DML 예제는 독립적인 테스트 환경에서 사용한다. 1. B-tree와 BRIN이 읽는 단위 서로 다른 문제를 해결한다 기술 핵심 질문 해결 범위 남는 문제 파티셔닝 어느 데이터 구역인가? 테이블 관리·Pruning 단일 서버 자원 한계 샤딩 어느 DB 노드인가? 데이터·부하 수평 확장 분산 운영·조인·트랜잭션 B-tree 어떤 키의 행인가? 정렬된 키 탐색 인덱스 크기·변경 비용 BRIN 어디를 안 읽어도 되는가? 페이지 구간 제외 후보 행 재검사·배치 의존 세 가지 분할·검색 기술은 배타적이지 않다. 파티션 안에 B-tree와 BRIN을 모두 둘 수도 있지만 각 인덱스가 해결할 쿼리와 쓰기 비용을 따져야 한다. B-tree의 내부 직관 루트와 내부 노드는 탐색할 하위 범위를 안내한다. 리프에는 키와 heap 행 위치 정보가 있으며 범위 검색은 리프를 이어서 탐색할 수 있다. 실제 PostgreSQL 구현은 중복 키 처리 등 최적화가 있으므로 모든 행이 반드시 독립적인 같은 크기의 리프 항목 하나를 갖는다는 뜻은 아니다. 인덱스 키의 정렬과 heap 물리적 순서는 별개다. Index Only Scan이 가능한 경우에도 필요한 컬럼과 visibility map 등의 조건이 관련되며, B-tree 사용 자체가 heap 접근 0회를 보장하지 않는다. 조회 설계 후보 request_id 하나 조회 request_id B-tree 고객별 최신 20건 (customer_id, created_at DESC) B-tree 범위·정렬·제한 건수 쿼리 정렬과 맞는 B-tree 유일성 UNIQUE B-tree BRIN의 내부 직관 BRIN은 인접한 heap 페이지 구간마다 요약을 저장한다. 대표적인 minmax는 최솟값과 최댓값이다. 조건에 맞을 가능성이 없는 구간을 제외하고, 남은 페이지의 실제 행을 재검사한다. 구간 시간 요약 9월 20일 조건 A 9월 1~5일 제외 B 9월 6~10일 제외 C 9월 11~15일 제외 D 9월 16~21일 후보, 실제 행 재확인 요약만 보면 D에 20일이 반드시 있다는 보장은 없다. BRIN은 행 위치 목록을 주는 방식이 아니므로 Bitmap Heap Scan과 재검사가 자연스럽다. lossy는 후보가 넓다는 뜻이며 최종 결과가 틀린다는 뜻이 아니다. 물리적 상관관계 시간순 append 데이터라면 각 구간의 시간 폭이 좁아 minmax가 효과적일 수 있다. 반면 모든 구간에 1월부터 12월까지 섞이면 9월 조회에서 대부분을 읽게 된다. 과거 데이터 backfill과 UPDATE도 분포를 바꿀 수 있다. ANALYZE api_request_history_202609; SELECT attname, correlation FROM pg_stats WHERE schemaname = 'public' AND tablename = 'api_request_history_202609' AND attname = 'created_at' AND inherited = false; correlation은 -1~1의 통계이며 절댓값이 크면 순서의 연관이 강하다. 이것은 BRIN 합격 점수가 아니다. 구간 내 군집과 operator class에 따라 결과가 달라지므로 실제 읽기량을 측정한다. bloom에는 minmax와 동일한 물리적 정렬 전제를 적용하지 않는다. B-tree와 BRIN 비교 항목 B-tree BRIN 요약 단위 키와 행 위치 중심 여러 페이지 구간 단건 조회 일반적으로 우선 검토 후보 페이지 재검사 부담 가능 큰 시간 범위 선택도·배치에 따라 판단 배치가 맞으면 유리한 후보 정렬 결과 인덱스 순서 활용 가능 정렬 순서 제공 용도 아님 공간 데이터·키 수 영향 큼 보통 작지만 설정·클래스에 좌우 쓰기 인덱스 항목 유지 비용 요약 갱신·요약 생성 비용 BRIN이 작다고 모든 범위 쿼리가 B-tree보다 빠르지는 않다. 결과가 테이블 대부분이면 Seq Scan이 더 효율적일 수도 있다. 2. BRIN의 요약 방식과 설정 학습 자료의 개념도 — B-tree와 BRIN의 탐색 단위. 세부 조건은 본문 설명을 함께 읽는다. 오퍼레이터 클래스란? 인덱스가 특정 자료형을 어떤 방식으로 표현하고 어떤 연산을 처리할지 정의한다. BRIN에서는 ‘구간을 어떻게 요약하는가’와 ‘어떤 비교로 후보를 제외하는가’를 함께 결정한다. BRIN에는 inclusion 계열도 있지만 여기서는 학습한 minmax·minmax-multi·bloom 세 종류에 집중한다. 같은 데이터로 비교 한 물리적 페이지 구간에 10, 11, 12, 90, 91, 92 가 있고 value = 50 을 찾는다고 가정한다. 클래스 개념 요약 50 조회 minmax [10, 92] 범위 안이므로 읽고 재확인 minmax-multi 예: [10,12] , [90,92] 이 요약이라면 제외 가능 bloom 값들을 해시한 비트 배열 확실히 없음이면 제외, 가능성이 있으면 재확인 minmax-multi 표는 가능한 개념 예시다. 구현이 항상 두 구간으로 만들거나 모든 빈 공간을 정확히 보존한다는 뜻은 아니다. bloom 비트도 예시 값만으로 임의 확정하지 않는다. minmax 한 구간의 최솟값과 최댓값으로 요약한다. 단순하고 작은 표현이지만, 값들이 멀리 떨어져 있으면 중간의 비어 있는 영역까지 후보가 된다. 값의 물리적 정렬이 잘 유지되는 시간·순번 데이터가 전형적인 후보다. 10 12에 1,000이라는 값 하나가 추가되면 큰 범위가 10 1,000으로 늘어날 수 있다. 범위 안의 값이 실제로 존재하는지는 heap을 읽어야 한다. 동등 및 순서 비교 연산에 활용된다. minmax-multi 여러 작은 구간 또는 개별 값으로 분포를 표현해 이상값의 영향을 줄이는 방식이다. 물리적 페이지를 여러 개로 분할하거나 데이터를 정렬하는 기능이 아니다. 요약 공간은 제한된다. 값이 복잡하게 분산되면 구간이 병합되어 후보가 넓어질 수 있다. 따라서 ‘minmax의 항상 더 빠른 상위 호환’으로 생각하지 말고 요약 크기와 읽기 감소의 균형을 측정한다. values_per_range 는 저장할 값 표현의 예산이다. 하나의 값은 점이나 구간 경계가 될 수 있다. 32로 설정했다고 32개 구간이 생기는 것은 아니다. PostgreSQL 18 기준 기본 32, 범위 8~256이다. bloom 페이지 구간의 값들을 해시해 Bloom filter로 요약한다. 검색값에 필요한 비트 중 하나라도 0이면 해당 값은 없다고 판정한다. 모두 1이면 실제로 있을 수도, 다른 값의 해시가 비트를 채운 오탐일 수도 있으므로 실제 행을 확인한다. 판정 의미 다음 작업 확실히 없음 요약된 구간에 검색값 없음 구간 제외 있을 수 있음 존재 또는 오탐 페이지 읽고 검사 정상적으로 유지되는 필터에서 false positive는 가능하지만 false negative는 없다. 요약되지 않은 BRIN 구간은 안전하게 후보로 처리되어 정답 누락이 아니라 추가 읽기 문제가 된다. bloom 클래스는 = 검색용이며 < , > , BETWEEN을 직접 지원하는 범위 인덱스가 아니다. 물리적 순서가 약한 데이터에도 후보가 될 수 있지만 모든 구간에 검색값이 실제 존재하거나 구간별 고유값이 많으면 읽기 감소가 제한된다. 랜덤 UUID라고 무조건 B-tree보다 좋지는 않다. 또한 USING brin (... uuid_bloom_ops) 와 별도 bloom 확장의 USING bloom 은 서로 다른 인덱스 방식이다. 파라미터를 구분하자 파라미터 위치 의미 조정 효과 pages_per_range BRIN 인덱스 한 요약이 담당하는 heap 페이지 수 작으면 세밀하지만 항목 증가 autosummarize BRIN 인덱스 새 구간 요약 요청 자동화 즉시·동기 완료 보장 아님 values_per_range minmax-multi 클래스 값·경계를 저장할 예산 크면 표현력과 크기 증가 가능 n_distinct_per_range bloom 클래스 구간별 예상 고유값 수 필터 크기 산정에 사용 false_positive_rate bloom 클래스 목표 오탐률 낮게 잡으면 더 큰 필터 필요 PostgreSQL 18의 bloom 목표 오탐률 기본값은 0.01, 허용 범위는 0.0001~0.25다. 이것은 인덱스 설계 입력값이며 실제 업무 쿼리의 관측 오탐률을 보증하지 않는다. 구간별 distinct 추정이 틀리면 예상과 다를 수 있다. 생성 예시 아래 인덱스는 같은 컬럼에서 비교할 대안 이다. 운영에서 전부 생성하라는 뜻은 아니다. v 는 bigint라고 가정한다. -- 기본 minmax CREATE INDEX idx_events_minmax ON events USING brin (v int8_minmax_ops) WITH (pages_per_range = 128, autosummarize = on); -- 여러 값·경계 요약 CREATE INDEX idx_events_multi ON events USING brin ( v int8_minmax_multi_ops (values_per_range = 64) ) WITH (pages_per_range = 128, autosummarize = on); -- 존재 가능성 요약: 숫자는 설명용이며 실제 distinct로 조정 CREATE INDEX idx_events_bloom ON events USING brin ( v int8_bloom_ops ( n_distinct_per_range = 1000, false_positive_rate = 0.01 ) ) WITH (pages_per_range = 128, autosummarize = on); bigint에는 int8_* , integer에는 int4_* 처럼 자료형에 맞는 이름을 사용한다. 설치된 서버에서 확인할 수 있다. SELECT opc.opcname, opc.opcintype::regtype AS input_type, opc.opcdefault FROM pg_opclass opc JOIN pg_am am ON am.oid = opc.opcmethod WHERE am.amname = 'brin' ORDER BY input_type::text, opc.opcname; 유지보수 새 구간의 초기 요약은 VACUUM·autovacuum 또는 요약 함수로 수행된다. autosummarize를 켜도 요청 큐와 worker 일정 때문에 지연될 수 있다. SELECT brin_summarize_new_values('idx_events_minmax'::regclass); 이 함수는 미요약 구간을 처리한다. 이미 지나치게 넓어진 모든 요약을 자동 축소하는 명령은 아니다. 변경·삭제 후 요약의 품질이 나쁘다면 대상 구간의 desummarize 후 재요약이나 재구성을 별도 검토한다. 재요약해도 실제 물리적 데이터 분포가 나쁘면 근본 문제가 해결되지 않는다. 선택 기준 시간순 데이터가 깔끔하게 누적됨: minmax부터 검토. 대부분 질서가 있으나 이상값·군집이 섞임: minmax-multi 비교. 동등 검색이며 물리적 정렬이 약함: bloom과 B-tree 비교. 최신 N건·정렬·단건 지연이 중요함: B-tree를 먼저 검토. 출처: BRIN 본문과 operator class parameters . 파라미터 기본값과 허용 범위는 PostgreSQL 18 기준. 3. 파티셔닝으로 조회·관리 단위 나누기 학습 자료의 개념도 — BRIN 요약 방식 비교. 세부 조건은 본문 설명을 함께 읽는다. 정의와 필요성 파티셔닝은 논리적으로 하나의 테이블을 여러 물리적 구역으로 나누는 기능이다. 애플리케이션은 부모 테이블을 조회하고, 실제 행은 리프 파티션에 저장된다. 부모는 자체 행 저장 공간을 갖지 않는다. 핵심 목적은 조회 대상 축소와 데이터 관리 단위 분리다. 예를 들어 하루 1,000만 건이면 365일에 36억 5,000만 건이다. 최근 며칠을 조회하고 오래된 월별 데이터를 지우는 요구가 있다면, 월별 구역을 나누는 것이 관리에 도움이 될 수 있다. 이 수치는 규모를 설명하기 위한 예이며 도입 기준은 아니다. 대량 DELETE는 행을 하나씩 처리하고 MVCC상 정리 가능한 이전 버전이 남는다. WAL과 복제 부하, VACUUM 부담도 고려해야 한다. 일반 VACUUM은 주로 내부 공간 재사용을 가능하게 하며 파일 전체를 자동으로 압축하는 작업과 다르다. 분할 방식 방식 기준 적용 후보 주의 RANGE 시간·숫자 범위 이벤트·이력·정산 하한 포함, 상한 제외 LIST 명시적 값 목록 국가·업무 분류 그룹 증가·쏠림 관리 HASH 키 해시값의 나머지 고객 키 중심 접근 단일 DB 내부 HASH는 샤딩과 다름 RANGE의 9월 구간은 [9월 1일, 10월 1일) 이다. 10월 1일 0시는 10월에 속한다. HASH는 원래 ID를 단순히 나눈 값이 아니라 PostgreSQL의 키 해시를 사용한다. 하나의 대형 고객은 같은 키로 묶이므로 쏠림이 남을 수 있다. 생성과 저장 위치 확인 다음 예제는 UTC 월 경계다. 업무 기준이 한국 시간이면 모든 경계와 쿼리의 시간대 정의를 일관되게 바꾼다. CREATE TABLE api_request_history ( request_id bigint NOT NULL, created_at timestamptz NOT NULL, service_id bigint NOT NULL, status_code integer NOT NULL, latency_ms integer, PRIMARY KEY (created_at, request_id) ) PARTITION BY RANGE (created_at); CREATE TABLE api_request_history_202609 PARTITION OF api_request_history FOR VALUES FROM ('2026-09-01 00:00:00+00') TO ('2026-10-01 00:00:00+00'); CREATE TABLE api_request_history_202610 PARTITION OF api_request_history FOR VALUES FROM ('2026-10-01 00:00:00+00') TO ('2026-11-01 00:00:00+00'); CREATE INDEX idx_history_service_time ON api_request_history (service_id, created_at); INSERT INTO api_request_history VALUES (1001, '2026-09-27 03:00:00+00', 10, 200, 125); SELECT tableoid::regclass AS physical_table, request_id, created_at FROM api_request_history; 저장 위치는 9월 파티션이다. 부모 인덱스 선언에 대응하는 실제 인덱스도 파티션에 생성된다. 범위에 맞는 파티션이 없고 DEFAULT도 없다면 INSERT는 실패한다. 미래 파티션 생성은 별도 자동화 작업이다. Pruning과 인덱스의 관계 EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM api_request_history WHERE service_id = 10 AND created_at >= TIMESTAMPTZ '2026-09-20 00:00:00+00' AND created_at < TIMESTAMPTZ '2026-09-21 00:00:00+00'; Pruning은 결과가 존재할 수 없는 파티션을 제거한다. 이후 남은 파티션에서 Index Scan, Bitmap Scan, Seq Scan 등을 비용에 따라 선택한다. Pruning 자체는 인덱스를 요구하지 않는다. 관찰 대상 의미 실행 계획의 파티션 이름 어떤 구역이 조회되었는가 Subplans Removed 실행 초기 Pruning의 단서 loops, never executed 실행 중 Pruning 등을 해석할 단서 Planning Time 분할 수에 따른 계획 비용도 점검 Buffers 접근 페이지 수 관찰 파라미터가 실행 시 알려져도 Pruning이 가능한 경우가 있다. 단, never executed 하나만으로 Pruning을 확정하지 말고 전체 계획을 본다. 시간 키로 분할했는데 WHERE request_id = 1001 만 제공하면 어느 달인지 알 수 없어 여러 파티션을 탐색할 수 있다. 또한 date_trunc('month', created_at) 처럼 키를 함수로 감싼 조건보다 원래 키에 직접 반개방 범위를 주는 것이 명확하다. PRIMARY KEY와 전역 유일성 부모 테이블의 UNIQUE/PRIMARY KEY에는 파티션 키의 모든 컬럼이 포함되어야 한다. 이 제약을 사용하는 경우 파티션 키의 표현식·함수 사용에도 제한이 있다. PRIMARY KEY (created_at, request_id) 는 두 값의 조합을 보장한다. 다음 두 행은 서로 다른 키다. created_at request_id 2026-09-27 1001 2026-10-03 1001 따라서 request_id 단독의 전역 유일성을 보장한 것이 아니다. 시퀀스나 UUID 발급 정책과 DB 제약은 구분한다. 외래 키로 ID 하나만 참조할 필요가 크다면 다음 구성을 검토한다. 테이블 책임 requests 요청 ID의 유일성, 현재 상태, 참조 기준 request_events 시간별 상세 이력과 보관 기간 일별·월별 선택 요구 판단 예시 최근 15일 보관 일별 파티션이 만료 처리에 잘 맞음 월 단위 정산·보관 월별 후보 하루 데이터가 매우 큼 더 작은 관리 단위 검토 작은 데이터 장기 보관 과도한 세분화 지양 월별 파티션에서 월 중간의 만료일을 정확히 지키려면 경계 파티션에서 DELETE가 필요할 수 있다. 또는 보관 정책이 허용하는 경우 더 오래 보관한다. 일별도 정확한 초 단위 rolling retention과 완전히 같지는 않다. 제거·분리·연결 아래는 운영 절차 설명이며 보관 정책상 만료된 파티션에만 적용한다. -- 부모 조회에서 분리하되 독립 테이블로 보존 ALTER TABLE api_request_history DETACH PARTITION api_request_history_202609; -- 백업 및 삭제 결정이 완료된 뒤에만 별도로 실행 -- DROP TABLE api_request_history_202609; 직접 DROP이나 일반 DETACH에는 부모 잠금 영향을 고려한다. DETACH CONCURRENTLY는 잠금 수준을 낮추지만 트랜잭션 블록에서 사용할 수 없고 DEFAULT 파티션이 있으면 사용할 수 없는 등 제약이 있다. 대량 적재는 독립 테이블에 적재·검증 후 ATTACH하는 방법도 있다. 경계에 맞는 유효한 CHECK 제약이 있으면 ATTACH 검증 스캔을 피하는 데 도움이 된다. CHECK가 없거나 DEFAULT 검증이 필요하면 스캔·잠금 비용이 커질 수 있다. 운영 함정 DEFAULT는 범위 밖 입력을 받을 수 있지만, 미래 파티션 생성 실패를 숨길 수 있다. 행 수와 유입을 감시한다. DEFAULT에 이미 들어간 행은 새 파티션을 만들었다고 자동 이동하지 않는다. 파티션 키 UPDATE로 경계를 넘으면 행 이동이 발생할 수 있다. 안정적인 키가 운영에 유리하다. 파티션별 UPDATE·DELETE에는 여전히 VACUUM이 필요하다. 오래된 파티션도
velog
이 글에서 다룰 주제 공간: 행 수가 일정한데 파일이 커지는 이유는? 프리징: 삭제할 행이 적어도 유지보수가 필요한 이유는? 운영: 어떤 지표와 방해 요인을 함께 점검해야 하는가? 주요 단어 · VACUUM · Bloat · Autovacuum · XID Age · Freeze · Visibility Map 행 수가 일정하다고 파일 크기까지 일정한 것은 아니다. 반대로 dead tuple이 거의 없다고 유지보수가 필요 없는 것도 아니다. PostgreSQL의 유지보수에는 더 이상 필요하지 않은 버전의 공간을 재사용하는 일 과 오래된 트랜잭션 참조를 안전하게 처리하는 일 이 함께 들어 있다. VACUUM을 단순한 파일 압축 도구로 보면 이 두 문제를 놓치기 쉽다. VACUUM 은 불필요한 버전 정리와 공간 재사용, 프리징 등을 수행한다. Freeze 는 충분히 오래된 생성 트랜잭션을 일반 XID 비교의 대상에서 안전하게 제외하는 처리다. 자료와 예제 기준 — PostgreSQL 18을 중심으로 개인 학습 노트를 재구성했다. SQL·실행 계획·설정값은 설명 및 재현용 예제이며 이 글을 위해 운영 DB에서 새로 측정한 결과는 아니다. DDL/DML 예제는 독립적인 테스트 환경에서 사용한다. 1. Heap과 Index는 따로 부푼다 행 수가 그대로여도 이전 버전과 비효율적인 빈 공간 때문에 파일이 커질 수 있다. 구분 Table Bloat Index Bloat 대상 heap 페이지 인덱스 페이지 원인 예 정리 지연, 공간 재사용 불균형 이전 TID 엔트리, 낮은 페이지 밀도 영향 테이블 스캔·캐시 효율 저하 인덱스 스캔·캐시 효율 저하 HOT와 관계 새 버전은 여전히 생성됨 새 일반 인덱스 엔트리 생성을 줄임 non-HOT UPDATE에서는 키 값이 그대로여도 새 TID를 가리키는 인덱스 엔트리가 필요할 수 있다. PK의 논리적 유일성을 지키면서 여러 MVCC 버전에 대응하는 물리 엔트리가 존재할 수 있다. dead bytes + free bytes 를 곧바로 OS에 반환 가능한 공간으로 해석하지 않는다. fillfactor 여유, 오버헤드, 페이지별 분포, 곧 재사용될 공간을 구분해야 한다. Dead 비율 ≠ bloat 비율 — n_dead_tup은 행 개수 추정치다. 행 크기·빈 공간·인덱스·TOAST까지 설명하지 못한다. 빈 공간의 해석 — 곧 재사용하거나 fillfactor로 의도한 여유라면 모두 낭비는 아니다. 추이와 조회 비용을 함께 본다. 인덱스의 자체 관리 — B-tree에는 불필요한 엔트리 삭제·중복 제거 최적화도 있다. UPDATE마다 파일이 반드시 커지지는 않는다. 기억할 문장: 공간 크기보다 “다시 쓰이는가, 읽기 비용이 커지는가”를 확인하자. 2. 재사용할까, 파일을 줄일까? 세 방법의 선택 기준은 공간 반환의 필요성과 잠금 허용 시간이다. 기준 VACUUM VACUUM FULL pg_repack 방식 기존 페이지 정리 전체 재작성 복사·변경 반영·교체 목적 내부 공간 재사용 파일 축소 온라인 재구성·축소 일반 접근 대부분 읽기·쓰기 가능 대상 테이블 접근 차단 대부분 가능, 시작·마무리 강한 잠금 추가 공간 전체 복사본 불필요 새 파일 공간 필요 복사본·인덱스·변경 로그 등 필요 선택 일상 유지보수 점검 시간 확보 중단이 어려운 테이블 일반 VACUUM도 파일 끝 빈 페이지를 반환할 때 강한 잠금을 사용할 수 있다. TRUNCATE FALSE 는 이 동작을 생략한다. 일반 VACUUM은 보통 파일 전체를 축소하지 않는다. pg_repack은 트리거로 변경을 기록하고, 복사본에 적용한 뒤 교체한다. --no-kill-backend 는 잠금 충돌 시 다른 쿼리·세션을 강제로 취소·종료하는 기본 정책을 피하는 데 사용한다. 설치 버전과 옵션을 확인한다. FULL과 repack 이후에도 autovacuum이 필요하다. 일반 VACUUM — 인덱스 정리 후 DEAD 슬롯을 재사용 가능하게 할 수 있다. 프리징·Visibility Map도 관리한다. 추가 디스크 — FULL과 repack은 기존 파일을 유지한 채 새 파일을 만든다. 디스크가 가득 찬 상황의 즉시 해법이 아니다. pg_repack 조건 — 전체 테이블 재구성에는 PK 또는 적격 NOT NULL UNIQUE 키가 필요하다. 확장·클라이언트 버전도 맞춘다. 기억할 문장: 인덱스만 비대하면 REINDEX CONCURRENTLY를 별도로 검토한다. 3. 공간 재사용과 autovacuum VACUUM과 autovacuum 1 제거 가능한 시점을 판단해야 한다 최신 버전이 아니라는 이유만으로 이전 버전을 제거할 수 없다. 오래된 스냅샷에서 여전히 필요할 수 있기 때문이다. 더 이상 필요하지 않은 버전을 정리해 공간을 재사용한다. 기능 결과 불필요한 튜플·인덱스 엔트리 정리 재사용 공간 확보 Visibility Map 관리 Index-only scan에서 Heap 확인 감소 Freeze 오래된 XID의 순환 문제 방지 FSM 갱신 새 튜플이 들어갈 여유 페이지 탐색 지원 VACUUM 자체도 영구 저장 구조를 바꾸므로 WAL과 Dirty page를 만들 수 있다. ‘청소는 I/O가 없다’고 가정하면 안 된다. 2 일반 VACUUM과 FULL 항목 VACUUM VACUUM FULL 방식 기존 파일에서 버전 정리 새 파일로 재작성 공간 주로 내부 재사용 압축해 OS에 반환 DML 일반 읽기·쓰기와 병행 가능 ACCESS EXCLUSIVE로 차단 추가 공간 전체 재작성 복사본 불필요 새 테이블·인덱스를 위한 여유 필요 운영 목적 지속적인 유지보수 대규모 영구 감소 후 공간 회수 일반 VACUUM도 테이블 끝의 완전히 빈 페이지를 잘라내면 OS에 공간을 반환할 수 있다. 이 과정에는 강한 잠금이 필요할 수 있다. 따라서 ‘절대 파일이 줄지 않는다’, ‘어떤 잠금도 없다’는 설명은 부정확하다. FULL은 특정 인덱스 순서로 정렬하는 기능이 아니다. 새 파일을 만들 공간이 필요하므로 디스크가 꽉 찬 상태에서 무조건 실행하는 해결책도 아니다. Autovacuum은 FULL을 자동으로 실행하지 않는다. 3 VACUUM과 ANALYZE VACUUM orders; ANALYZE orders; VACUUM (ANALYZE) orders; ANALYZE는 컬럼 분포·히스토그램·빈도 등 플래너 통계를 수집한다. VACUUM도 일부 관계 통계를 갱신하지만 ANALYZE를 대체하지는 않는다. 파티션 부모의 통계는 자식 변경만으로 자동 갱신되지 않는 경우가 있어 별도 ANALYZE를 검토한다. 4 Visibility Map과 Freeze 일반 인덱스 엔트리만으로 MVCC 가시성을 완전히 알 수 없다. Index-only scan도 대상 페이지가 all-visible이 아니면 Heap을 확인할 수 있다. 따라서 인덱스가 모든 컬럼을 포함해도 Heap Fetches 가 발생한다. Freeze는 행을 수정 불가능하게 만드는 기능이 아니다. 오래된 생성 트랜잭션을 모든 적절한 미래 스냅샷보다 과거인 것으로 취급하도록 표시한다. 내부 XID의 순환에 대비하는 유지보수이므로 UPDATE·DELETE가 거의 없는 테이블도 필요하다. 5 Autovacuum의 실행과 처리 능력 기본적인 변경량 조건: base threshold + scale factor × estimated tuples PostgreSQL 18에서는 최대 임계값 설정도 적용된다. INSERT 기반 조건과 Freeze 관련 조건은 별도로 존재한다. 1,000만 행, base=50, scale=0.2이면 약 2,000,050개의 obsolete tuple 발생 조건을 계산한다. 이는 실행 가능성을 판단하는 기준이지 즉시 시작·완료 보장이 아니다. -- 부하 측정 후 조정할 교육용 예시 ALTER TABLE orders SET ( autovacuum_vacuum_threshold = 1000, autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.02 ); 임계값을 낮추면 더 자주 처리하지만 Worker·I/O·잠금 제약을 함께 봐야 한다. 오래된 스냅샷 때문에 제거 불가능한 버전은 Worker만 늘려도 정리되지 않는다. 복제 피드백, Slot이 유지하는 xmin, prepared transaction 등도 보존 경계에 영향을 줄 수 있다. 4. 오래된 미동결 XID를 관리해야 하는 이유 프리징 자체가 위험한 기능은 아니다. 오래된 미동결 XID가 계속 남아 있으면 번호 비교의 안전 범위를 벗어날 수 있고, PostgreSQL은 이를 막기 위해 신규 XID 발급을 차단할 수 있다. 애플리케이션에서는 INSERT·UPDATE 등의 실패로 나타난다. 이 문제의 시계는 날짜가 아니라 XID 소비량 으로 움직인다. 부하가 높은 시스템은 같은 시간을 운영해도 훨씬 빨리 위험 구간에 접근한다. 일반적인 서버 재시작으로 XID 나이나 미동결 데이터가 초기화되지는 않는다. 유지보수 경계가 실제로 전진해야 한다. 5. MVCC와 XID의 관계 PostgreSQL은 UPDATE 시 새로운 행 버전을 만들고, 트랜잭션 스냅샷에 따라 보이는 버전을 결정한다. 항목 의미 주의할 점 xmin 해당 행 버전을 생성한 트랜잭션 원래 숫자만으로 동결 상태를 판정할 수 없음 xmax 삭제·갱신 또는 행 잠금 관련 정보 0이 아닌 값이 있다고 무조건 삭제된 행은 아님 Snapshot 현재 조회에서 보이는 트랜잭션 범위와 진행 중 트랜잭션 정보 단순 숫자 비교만으로 가시성이 결정되지는 않음 xid 내부 32비트 트랜잭션 식별자 번호가 순환함 xid8 epoch를 포함한 64비트 식별자 내부 튜플의 프리징 필요성을 없애지는 않음 Virtual XID 트랜잭션을 식별하는 가상 식별자 영구 XID와 구분 예를 들어 xmin = 500 인 행 버전은 XID 500이 생성했다는 뜻이다. PostgreSQL은 그 트랜잭션의 커밋 여부, 스냅샷과의 관계, 삭제·갱신 상태 등을 함께 판단한다. 영구 XID는 일반적으로 최초 쓰기 시 발급된다. 따라서 SELECT 요청 수나 전체 커밋 건수를 XID 소비량과 동일하게 취급하면 안 된다. 서브트랜잭션이나 XID 할당을 유발하는 함수도 측정에 영향을 줄 수 있다. XID 카운터는 같은 PostgreSQL 클러스터의 모든 데이터베이스가 공유 한다. 한 테이블이 변경되지 않아도 다른 곳의 쓰기로 그 테이블의 XID 나이가 증가할 수 있다. 여기서 클러스터는 Kubernetes 클러스터가 아니라 하나의 PostgreSQL 데이터 디렉터리로 관리되는 데이터베이스 집합을 뜻한다. 6. 42.9억 번호 공간과 21.5억 비교 한계 32비트 번호 공간은 2^32 = 4,294,967,296 , 약 42.9억이다. 일부 특수 XID는 예약되어 있지만 전체 크기를 이해할 때는 이 숫자로 생각하면 된다. 그러나 과거·미래 비교가 가능한 범위는 대략 절반인 2^31 = 2,147,483,648 , 약 21.5억이다. 순환하는 번호 공간에서 한쪽 반 바퀴를 과거, 다른 반 바퀴를 미래로 해석하기 때문이다. 1 0~99 번호표로 축소한 예 생성 번호가 20인 행이 프리징되지 않은 채 남아 있다고 가정한다. 현재 번호 생성 번호 앞으로 진행한 거리 해석 30 20 10 생성 번호는 과거 60 20 40 아직 과거로 비교 가능 80 20 60 반 바퀴를 넘어 정상적인 과거 해석이 불가능 즉, 최댓값에서 0으로 돌아가는 순간만 감시하면 안 된다. 현재 XID와 가장 오래된 미동결 XID 경계 사이의 거리 가 중요하다. XID 순환은 정상적인 장기 운영에서도 일어난다. 위험한 상황은 순환 자체보다 오래된 XID 참조를 안전하게 처리하지 못한 상태 다. 7. 프리징의 실제 의미 학습 자료의 개념도 — VACUUM·FULL·pg_repack의 목적. 세부 조건은 본문 설명을 함께 읽는다. 프리징 전에는 행 생성 트랜잭션의 상태와 가시성을 일반 XID 규칙으로 판단한다. 프리징 후에는 생성 트랜잭션을 모든 일반 XID보다 오래된 것으로 취급한다. 구분 행 생성 트랜잭션의 의미 프리징 전 ‘XID 500이 만든 행’이라는 일반 트랜잭션 참조 프리징 후 ‘충분히 오래전에 커밋된 생성 트랜잭션’으로 확정 개념적으로는 FrozenTransactionId 로 취급한다. PostgreSQL 9.4 이후에는 원래 xmin 을 보존하고 튜플 헤더의 동결 플래그로 표현하므로, 프리징 후에도 SELECT xmin 결과가 500으로 남을 수 있다. 프리징은 생성 트랜잭션의 가시성 문제를 해결 하는 것이다. 행이 이후 삭제되거나 갱신되면 그 상태에 맞는 가시성 판단은 계속 필요하다. 데이터가 영구적으로 모든 조회에 보인다거나 수정 불가능해진다는 뜻이 아니다. 진행 중인 트랜잭션과 오래된 스냅샷 등이 안전한 처리 범위를 제한한다. FREEZE 옵션도 이 안전 조건을 무시하지 않는다. 8. VACUUM과 Visibility Map 1 VACUUM이 맡는 서로 다른 역할 작업 해결하려는 문제 죽은 튜플 정리 더 이상 필요 없는 행 버전이 차지하는 공간 프리징 오래된 트랜잭션 참조와 번호 순환 Visibility Map 갱신 페이지 상태를 이용한 조회·유지보수 최적화 ANALYZE 플래너 통계 갱신. 별도 명령이며 VACUUM과 함께 실행 가능 따라서 dead tuple이 거의 없다는 사실만으로 프리징이 불필요하다고 결론 내릴 수 없다. 2 all-visible과 all-frozen 상태 의미 활용 all-visible 페이지의 튜플이 모든 관련 트랜잭션에 보이는 상태 Index Only Scan의 heap 접근 생략에 활용 all-frozen 페이지의 튜플이 모두 동결된 상태 동결을 위한 재검사 생략에 활용 all-visible이라고 all-frozen인 것은 아니다. 일반 VACUUM은 일부 페이지를 건너뛸 수 있지만, aggressive VACUUM은 미동결 XID가 있을 수 있는 페이지까지 확인한다. 이미 all-frozen인 페이지는 보통 다시 검사할 필요가 없다. PostgreSQL 18에는 일반 VACUUM 중 일부 all-visible 페이지를 미리 동결하려는 eager freezing도 있다. 세부 검사 정책은 버전에 따라 다르므로, ‘일반 VACUUM은 프리징을 전혀 하지 않는다’고 이해하면 안 된다. 3 VACUUM FREEZE와 VACUUM FULL -- 예시: 지정한 테이블을 적극적으로 동결 VACUUM (FREEZE, VERBOSE) public.target_table; FREEZE 는 동결 관련 나이 기준을 낮추어 안전하게 처리 가능한 튜플을 적극적으로 동결한다. VACUUM FULL 은 테이블을 재작성하여 공간을 축소하는 작업으로, 목적과 잠금 비용이 다르다. XID 위험 대응에서 FULL을 기본 처방으로 선택하지 않는다. 일반 VACUUM은 보통 읽기·쓰기와 함께 실행할 수 있지만 I/O를 사용하고 DDL 등과 충돌할 수 있다. 테이블 끝의 빈 페이지를 잘라내는 단계에서는 더 강한 잠금이 필요할 수 있다. 9. 자동 유지보수 설정과 트레이드오프 아래는 PostgreSQL 18 기본값이다. 단위는 초·일·행 수가 아닌 XID 나이다. 설정 기본값 역할 vacuum_freeze_min_age 50,000,000 프리징 여부를 결정하는 나이 기준 vacuum_freeze_table_age 150,000,000 실행되는 VACUUM이 적극적인 검사 전략을 선택하는 기준 autovacuum_freeze_max_age 200,000,000 wraparound 방지용 autovacuum 실행 대상이 되는 기준 vacuum_failsafe_age 1,600,000,000 VACUUM이 비상 전략으로 동결 진전을 우선하는 기준 2억은 쓰기 중단 한계가 아니라 예방 작업을 시작하는 기준이다. vacuum_freeze_table_age 는 별도의 스케줄러가 아니며, VACUUM의 실행 방식에 영향을 준다. failsafe는 비용 기반 지연을 중단하고 인덱스 정리 등 일부 부가 작업을 건너뛰어 위험을 줄이려 한다. 설정 간에는 유효 범위 보정도 있으므로 숫자를 각각 독립적으로 해석하지 않는다. 조정 방향 기대 효과 비용·주의점 더 일찍 프리징 나중에 몰리는 작업량 완화 곧 변경될 행까지 처리하면 불필요한 작업 증가 방지용 실행 기준을 늦춤 일부 테이블의 강제 유지보수 빈도 감소 상태 보존량 증가, 대응 여유와 처리량 검토 필요 VACUUM 처리량 확대 오래된 XID 경계가 빨리 전진 서비스 I/O·캐시 사용과 경쟁 autovacuum을 꺼도 wraparound 방지용 프로세스는 실행될 수 있다. 그러나 이 마지막 보호 장치에만 의존하는 운영은 바람직하지 않다. 10. 프리징이 밀리는 원인 1 오래 열린 트랜잭션 장기 트랜잭션이나 오래된 스냅샷은 VACUUM의 안전한 정리 경계를 붙잡을 수 있다. 단순히 연결이 오래 살아 있다는 것과 오래된 XID·스냅샷을 보유한다는 것은 구분해야 한다. 특히 idle in transaction 은 쿼리를 실행하지 않더라도 트랜잭션이 끝나지 않은 상태다. 실제 영향은 backend_xid , backend_xmin , 잠금 상태를 함께 보고 판단한다. 2 Prepared transaction 2단계 커밋의 prepared transaction은 일반 세션이 종료되어도 남을 수 있다. 업무 결과를 확인하고 COMMIT 또는 ROLLBACK해야 하며, 단순히 오래되었다는 이유로 일괄 처리해서는 안 된다. 3 복제 슬롯 슬롯 필드 주로 붙잡는 대상 restart_lsn 재시작에 필요한 WAL xmin VACUUM의 사용자 데이터 정리 경계 catalog_xmin 시스템 카탈로그 정리 경계 WAL 디스크 증가와 XID 프리징 정체는 관련될 수 있지만 같은 문제는 아니다. 슬롯의 실제 필드와 소비자 상태를 확인한다. 4 처리량 부족과 잠금 대형 테이블, 느린 스토리지, 제한된 worker·I/O 예산, 충돌하는 잠금, 반복 취소는 유지보수를 지연시킨다. ‘VACUUM이 실행 중’이라는 사실보다 완료 후 경계가 전진하는지 가 중요하다. 11. 읽기 전용 점검 SQL 아래 쿼리는 진단 예시다. 일부 세션 정보는 pg_monitor 등 적절한 권한이 필요하다. 데이터베이스 전체 진단과 테이블 진단의 범위를 구분한다. 1 실제 설정 확인 SELECT name, setting, unit, source FROM pg_settings WHERE name IN ( 'autovacuum', 'autovacuum_freeze_max_age', 'vacuum_freeze_min_age', 'vacuum_freeze_table_age', 'vacuum_failsafe_age' ) ORDER BY name; 테이블별 storage parameter가 전역 설정을 덮어쓸 수 있다는 점도 확인한다. 2 데이터베이스별 XID 나이 SELECT datname, age(datfrozenxid) AS xid_age FROM pg_database ORDER BY xid_age DESC; datfrozenxid 는 해당 DB에 남은 미동결 XID의 하한 경계다. 사용자 테이블뿐 아니라 카탈로그 등도 영향을 준다. 3 현재 DB의 테이블과 TOAST 나이 SELECT c.oid::regclass AS table_name, age(c.relfrozenxid) AS main_xid_age, age(t.relfrozenxid) AS toast_xid_age, GREATEST(age(c.relfrozenxid), age(t.relfrozenxid)) AS xid_age, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size, c.reloptions FROM pg_class AS c LEFT JOIN pg_class AS t ON t.oid = c.reltoastrelid WHERE c.relkind IN ('r', 'm') ORDER BY xid_age DESC LIMIT 30; relfrozenxid 는 그보다 오래된 XID가 안전하게 처리되었다고 보장하는 경계다. 원래 xmin 값의 단순 최솟값과 다르다. total_size 는 인덱스 등을 포함하므로 실제 heap 스캔량과 동일하지 않다. 파티션 부모보다 실제 데이터를 담는 자식 테이블의 상태를 확인한다. 4 오래된 트랜잭션·스냅샷 후보 SELECT pid, datnam
velog
이 글에서 다룰 주제 변경 기록: Shared Buffers와 WAL에는 각각 무엇이 남는가? 내구성: Commit은 무엇을 기다리는가? 페이지 기록: Checkpoint는 무엇을 보장하고 무엇을 보장하지 않는가? 주요 단어 · Dirty Page · WAL Buffers · WAL Flush · LSN · FPI · Commit · Checkpoint UPDATE와 COMMIT이 성공했다는 말은 변경된 데이터 페이지가 모두 디스크에 기록되었다는 말과 같지 않다. PostgreSQL은 복구에 필요한 로그를 먼저 보존하고, 데이터 페이지 쓰기를 다른 시점에 처리할 수 있다. 이 차이를 이해하면 WAL이 필요한 이유와 Checkpoint의 목적이 자연스럽게 연결된다. WAL(Write-Ahead Logging) 은 장애 후 변경을 재생할 수 있도록 남기는 로그이고, Dirty Page 는 메모리에서 변경되어 쓰기가 필요한 페이지다. Checkpoint 는 복구에 필요한 시작 기준과 데이터 페이지 기록을 관리한다. 자료와 예제 기준 — PostgreSQL 18을 중심으로 개인 학습 노트를 재구성했다. SQL·실행 계획·설정값은 설명 및 재현용 예제이며 이 글을 위해 운영 DB에서 새로 측정한 결과는 아니다. DDL/DML 예제는 독립적인 테스트 환경에서 사용한다. 1. Shared buffers와 WAL 학습 자료의 개념도 — WAL과 Commit의 순서. 세부 조건은 본문 설명을 함께 읽는다. 1 UPDATE가 발생했을 때 UPDATE orders SET status = 'PAID' WHERE id = 42; 일반적인 영구 Heap 테이블에서는 대상 페이지를 Shared buffers에서 다룬다. 기존 버전의 메타데이터를 갱신하고 새 버전을 만들며 관련 페이지를 Dirty로 표시한다. 인덱스 변경이 필요하면 인덱스 페이지도 영향을 받는다. 동시에 복구에 필요한 WAL이 생성된다. Dirty 표시만 하는 것이 아니다. 메모리의 실제 데이터 페이지가 변경된다. 2 Write-Ahead의 정확한 뜻 핵심 규칙은 다음과 같다. 변경된 데이터 페이지를 영속화하기 전에 그 변경을 복구할 관련 WAL이 먼저 영속화되어야 한다. 이는 메모리 페이지를 변경하기 전에 매번 디스크 WAL 동기화를 끝내야 한다는 뜻이 아니다. 순서 제약의 핵심은 영속화다. 성공 응답 부분은 기본 동기 커밋, 동기 복제 없음, 정상적인 저장장치 영속성 보장을 전제로 한다. 읽기 전용 트랜잭션 등은 같은 쓰기 경로를 모두 거치지 않는다. 3 WAL이 성능에 유리한 이유 커밋 때마다 변경된 여러 데이터 페이지를 모두 동기화하면 랜덤 I/O와 동기화 비용이 요청 지연에 직접 반영된다. WAL을 사용하면 커밋 경로에서 복구 로그의 영속화를 보장하고 데이터 페이지 쓰기를 분산할 수 있다. WAL은 순차적으로 추가 기록한다. 같은 데이터 페이지의 여러 변경을 메모리에 모을 수 있다. 여러 커밋이 한 번의 WAL 동기화를 공유하는 Group Commit이 가능하다. 데이터 파일과 WAL을 모두 쓰므로 총 기록 바이트가 항상 감소하는 것은 아니다. 4 Write와 Flush의 차이 단계 데이터 위치 장애 내구성 WAL buffers PostgreSQL 메모리 영속성 없음 파일 Write OS 캐시에 남아 있을 수 있음 OS·전원 장애 안전을 아직 보장하지 못함 WAL Flush 동기화 API를 통해 영속화 보장 저장장치가 계약을 지킨다는 전제에서 내구성 확보 ORM Flush와 WAL Flush는 이름만 같고 다른 작업이다. ORM Flush는 SQL 실행, WAL Flush는 로그 영속화를 뜻한다. 설정 역할 fsync=on 복구 일관성에 필요한 저장 동기화를 수행 synchronous_commit=on 성공 응답 전에 필요한 WAL 영속화 대기 synchronous_commit=off 최근 성공 트랜잭션의 장애 시 유실 가능성을 허용 full_page_writes=on 부분 기록 페이지를 복구하도록 전체 페이지 이미지 기록 wal_compression FPI 압축으로 WAL 크기 감소, CPU 비용 추가 fsync=off 는 단순히 최근 커밋 유실뿐 아니라 복구 불가능한 손상 위험까지 만든다. synchronous_commit=off 와 동등한 선택이 아니다. 동기 Standby가 설정되면 synchronous_commit=on 은 그 Standby의 WAL 영속화까지 기다릴 수 있다. remote_apply 는 적용까지 기다리고, local 은 로컬 영속화까지만 기다린다. 5 LSN·FPI·복구 LSN은 WAL의 위치이며 두 LSN의 차이는 WAL 바이트량으로 해석할 수 있다. 기본 WAL 세그먼트 크기는 16MB지만 클러스터 초기화 시 다른 크기를 지정할 수 있다. 데이터 페이지를 쓰던 중 일부만 기록되면 torn page가 생길 수 있다. full_page_writes 는 체크포인트 이후 페이지의 첫 변경에서 전체 페이지 이미지(FPI)를 기록해 복구를 돕는다. 작은 행 변경이어도 FPI 때문에 많은 WAL이 발생할 수 있다. 복구는 SQL 재실행이 아니라 WAL에 기록된 저장 구조 변경의 REDO다. WAL에는 미커밋 트랜잭션의 변경도 들어갈 수 있다. 복구 후 그 버전이 보이는지는 커밋 상태와 MVCC 규칙으로 판단한다. 2. Checkpoint와 completion target 학습 자료의 개념도 — Checkpoint의 쓰기 분산. 세부 조건은 본문 설명을 함께 읽는다. 1 Checkpoint가 해결하는 문제 데이터 페이지를 계속 메모리에만 두면 복구 때 오래된 WAL부터 처리해야 한다. Checkpoint는 필요한 페이지의 영속화를 완료해 안전한 복구 기준을 만든다. 시작 시 pg_control 과 체크포인트 레코드를 읽고, 레코드가 가리키는 REDO 위치부터 재생한다. 정확히는 ‘체크포인트 완료 시각 이후’가 아니라 체크포인트가 지정한 REDO 위치 이후 다. 시점 상황 10:00 체크포인트 시작 10:00~10:04 대상 페이지 기록, 일반 UPDATE도 계속 진행 10:04 체크포인트 완료 10:05 장애 복구 체크포인트 레코드의 REDO 위치부터 처리 체크포인트 진행 중에도 페이지가 새로 변경된다. 따라서 완료 순간 Shared buffers 전체가 Clean이거나 메모리와 디스크가 완전히 같은 상태일 필요는 없다. 2 페이지를 쓰는 프로세스 주체 목적 Checkpointer 복구 기준 확립을 위해 필요한 페이지 영속화 Background writer 버퍼 교체 시 요청 Backend가 쓰기를 떠안지 않도록 미리 기록 Client backend 필요에 따라 Dirty buffer를 직접 기록 Background writer는 Checkpoint의 대체 기능이 아니다. 너무 적극적으로 쓰면 자주 변경되는 페이지의 중간 상태를 반복 기록해 I/O가 증가할 수 있다. 3 세 가지 설정의 관계 설정 질문 PostgreSQL 18 문서 기본값 checkpoint_timeout 시간 기준으로 언제 시작할까? 5min max_wal_size WAL 발생량 때문에 앞당길까? 1GB checkpoint_completion_target 쓰기를 얼마나 길게 분산할까? 0.9 실제 값은 배포판·관리형 서비스·운영 설정에 따라 다르므로 SHOW 또는 pg_settings 로 확인한다. checkpoint_timeout = '5min' checkpoint_completion_target = 0.9 시간 기준 간격이 약 300초라면 대략 270초에 걸쳐 쓰기를 분산하는 목표다. 270초 기다렸다가 쓰는 것이 아니다. 해당 기간에 걸쳐 조금씩 기록한다. 6,000MB를 쓴다는 단순 가정: Target 목표 기간 단순 평균 쓰기량 0.3 90초 약 66.7MB/s 0.5 150초 약 40MB/s 0.9 270초 약 22.2MB/s 이 표는 설명용 계산이다. 실제 일정은 WAL 증가와 I/O 성능의 영향을 받으며, 이 설정이 고정 MB/s 제한은 아니다. WAL 용량 기준이 먼저 작동하면 항상 270초가 주어지지 않는다. 낮추면 빨리 끝내는 대신 I/O가 집중될 수 있다. 0.9는 쓰기를 넓게 분산하고 마무리 여유를 남기는 기본 출발점이다. 1.0은 마무리 작업과 변동을 위한 여유가 부족하다. 0.9는 페이지의 90%만 쓴다는 의미가 아니다. 이 값은 COMMIT을 Checkpoint 완료까지 기다리게 하지 않는다. 체크포인트를 너무 자주 하면 반복 페이지 쓰기와 FPI가 늘 수 있다. 간격을 늘리면 복구해야 할 WAL과 보존 공간이 증가할 수 있다. 성능과 복구 시간 목표를 함께 고려한다. 4 max_wal_size는 절대 상한이 아니다 다음 원인으로 pg_wal 사용량이 설정값보다 커질 수 있다. 아카이빙 실패·지연 Replication slot의 보존 요구 Standby 지연과 wal_keep_size 급격한 WAL 증가 WAL 생성 속도 증가 와 이미 생성된 WAL을 제거하지 못함 은 별도로 조사한다. Checkpoint나 VACUUM을 실행한다고 Slot이 요구하는 WAL까지 제거할 수 있는 것은 아니다. 3. 변경을 저장하는 과정 복습 UPDATE는 페이지의 실제 내용을 바꾸고 복구용 WAL을 만든다. 관련 WAL의 영속화는 해당 변경이 있는 데이터 페이지의 영속화보다 먼저다. 기본 동기 커밋에서는 필요한 커밋 WAL 영속화를 기다린다. 페이지 쓰기는 Commit 전후 모두 가능하며 Checkpoint만 담당하는 것도 아니다. WAL 보존량은 복제·아카이빙·슬롯 등의 조건도 영향을 받는다. 이 다섯 문장을 나누어 설명할 수 있으면 ‘WAL→데이터 파일’ 화살표를 일반 실행의 데이터 이동으로 오해하지 않을 수 있다. 자료 기준과 참고 문서 개인 PostgreSQL 학습 노트를 바탕으로 정리했다. 첨부 그림은 제공된 학습 자료를 사용했으며, 버전이나 설정에 따른 조건은 본문에 덧붙였다. WAL 개요 WAL과 Checkpoint WAL 설정 비동기 커밋 이어서 읽기 · ← 이전 편 · 다음 편 → · 전체 시리즈 목차
velog
이 글에서 다룰 주제 객체 구조: DB·스키마·권한과 저장 위치는 어떻게 다른가? 실행 구조: 연결을 받은 뒤 누가 SQL을 처리하는가? 메모리: 캐시와 정렬·해시 작업 공간은 무엇이 다른가? 주요 단어 · Cluster · Database · Schema · Role · Backend · Shared Buffers · work_mem pgAdmin을 열면 Tables, Schemas, Functions, Extensions처럼 익숙하면서도 역할이 다른 이름들이 한꺼번에 보인다. 여기에 Backend, Shared Buffers, WAL 같은 내부 구조 용어까지 더해지면 어디부터 연결해야 할지 헷갈리기 쉽다. 먼저 무엇을 저장하는가, 누가 실행하는가, 어디에서 처리하는가 를 나누어 PostgreSQL의 전체 지도를 그려 보자. Cluster 는 하나의 PostgreSQL 서버 인스턴스가 관리하는 데이터베이스 집합, Schema 는 DB 안의 이름 공간이다. Backend 는 클라이언트 연결의 SQL을 처리하는 서버 프로세스다. 자료와 예제 기준 — PostgreSQL 18을 중심으로 개인 학습 노트를 재구성했다. SQL·실행 계획·설정값은 설명 및 재현용 예제이며 이 글을 위해 운영 DB에서 새로 측정한 결과는 아니다. DDL/DML 예제는 독립적인 테스트 환경에서 사용한다. 1. pgAdmin에서 읽는 객체의 범위 화면에 보이는 것은 컬럼이 아니라 객체 종류다 pgAdmin에서 보이는 Tables·Schemas·Functions 등은 테이블 필드가 아니다. PostgreSQL의 여러 객체를 종류별로 묶어 보여주는 pgAdmin Object Explorer의 노드다. 실제 컬럼은 student 테이블을 펼친 뒤 Columns에서 확인한다. PostgreSQL의 database cluster는 한 서버 인스턴스가 관리하는 데이터베이스 집합을 뜻한다. 반드시 다중 서버나 분산 구성을 의미하지 않는다. mydb에 연결한 상태에서 student를 조회한다. SELECT * FROM public.student; public은 스키마, student는 테이블이다. 다른 데이터베이스의 테이블을 일반적인 database.schema.table 참조만으로 자유롭게 조회할 수 있는 구조가 아니다. 별도 연결이나 FDW 등이 필요하다. 서버와 DB 목록 항목 역할 해석 Servers (1) pgAdmin 서버 연결 목록 등록된 연결 1개 PostgreSQL 18 연결 표시 이름 이름은 변경 가능하므로 실제 버전은 별도 확인 Databases (2) DB 목록 예시: mydb, postgres mydb 업무·실습 DB 사용자가 만든 DB 이름의 예 postgres 보통 초기화 시 생성되는 기본 DB 관리 도구의 기본 접속 DB 등으로 사용 괄호 안 숫자는 표시된 객체 수이며 필터·시스템 객체 표시 설정의 영향을 받을 수 있다. SELECT version(); SELECT current_database(), current_user; SHOW search_path; 서버 범위의 Role과 Tablespace 항목 의미 Login/Group Roles 사용자 계정과 권한 그룹. LOGIN 속성이 있으면 접속 계정으로 사용 가능 Tablespaces 데이터 객체 파일을 저장할 물리 위치 pg_default 기본 저장 공간. 별도 지정이 없는 일반 객체의 기본 위치로 사용 pg_global Role 등 클러스터 공용 시스템 카탈로그 저장 공간 Role이 서버 범위 객체라고 해서 모든 DB·테이블에 자동 접근할 수 있는 것은 아니다. CONNECT·USAGE·SELECT 등 필요한 권한을 따로 가진다. CREATE ROLE app_reader NOLOGIN; CREATE ROLE report_user LOGIN; GRANT app_reader TO report_user; GRANT CONNECT ON DATABASE mydb TO app_reader; -- 이하 mydb에 연결한 상태 GRANT USAGE ON SCHEMA public TO app_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_reader; 이 예제는 비밀번호·인증 설정을 포함하지 않는다. 마지막 GRANT는 기존 테이블 대상이며 새 테이블은 생성 역할 기준의 ALTER DEFAULT PRIVILEGES 설정이 필요하다. 비교 Schema Tablespace 질문 어떤 이름 공간에 속하는가? 어느 물리 위치에 저장되는가? 예 school.student 특정 마운트 경로 목적 이름·권한·논리 구조 저장 장치 배치 관계 한 스키마 객체를 여러 Tablespace에 둘 수 있음 여러 스키마 객체를 같은 공간에 둘 수 있음 Tablespace는 샤딩 기능이 아니다. 디스크를 분리해도 서버의 CPU·메모리·장애 경계가 자동으로 분산되지 않는다. Schema와 public public은 보통 DB에 기본 생성되는 스키마다. 이름이 public이라고 누구나 모든 테이블에 접근할 수 있는 것은 아니다. 실제 권한을 확인해야 한다. CREATE SCHEMA school; CREATE TABLE school.student (id bigint PRIMARY KEY, name text); public.student와 school.student는 서로 다른 객체다. 스키마를 생략하면 search_path의 순서로 이름을 찾는다. 신뢰하지 않는 사용자가 객체를 만들 수 있는 스키마를 search_path에 넣으면 이름 가로채기 위험이 있으므로 서비스 SQL·권한 설계에서 주의한다. 2. 데이터와 로직을 구성하는 객체 학습 자료의 개념도 — Backend와 백그라운드 프로세스의 역할. 세부 조건은 본문 설명을 함께 읽는다. 데이터베이스 아래의 객체 항목 역할 동작 예와 주의점 Casts 타입 변환 규칙 '123'::integer 처럼 변환. 사용자 정의 Cast는 암묵적 변환에도 영향을 줄 수 있음 Catalogs DB 자체의 메타데이터 테이블·컬럼·권한·타입 등을 조회. 일반적으로 pg_catalog와 information_schema 표시 Event Triggers DDL 이벤트에 반응 CREATE·ALTER·DROP 등 지원되는 구조 변경 이벤트의 감사·통제 Extensions 확장 객체 패키지 타입·함수·연산자 등 관련 객체를 묶어 설치·버전 관리 Foreign Data Wrappers 외부 데이터 접근 구현 원격 DB·파일 등에 접근하는 어댑터 Languages 함수·프로시저 구현 언어 plpgsql 등. 자연어 설정과 다름 Publications 논리 복제 발행 범위 어떤 테이블의 어떤 변경을 발행할지 정의 Schemas 논리적 이름 공간 이름 충돌 방지와 권한 관리 Subscriptions 논리 복제 구독 원격 Publication의 데이터를 받아 적용 Catalogs: 데이터에 대한 데이터 student에 학생 정보가 저장된다면, 카탈로그에는 student의 컬럼·타입·제약 같은 구조 정보가 기록된다. SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'student' ORDER BY ordinal_position; pg_catalog: PostgreSQL 고유의 상세 시스템 정보. information_schema: 표준화된 메타데이터 인터페이스. 스키마 변경은 CREATE·ALTER·DROP으로 수행한다. 시스템 카탈로그 직접 수정으로 관리하지 않는다. Extension은 다른 객체 메뉴와 연결된다 확장이 등록한 함수는 Functions, 타입은 Types, 연산자는 Operators에 나타날 수 있다. 메뉴가 서로 다른 기능 제품을 뜻하는 것은 아니다. 하나의 기능이 여러 객체로 구성될 수 있다. 논리 복제 운영 DB의 Publication이 변경 범위를 정의하고, 수신 DB의 Subscription이 데이터를 가져와 적용한다. 최초 데이터 동기화 이후 변경 전달에 활용할 수 있다. DDL·시퀀스 상태까지 전부 자동 복제되는 것은 아니므로 스키마 배포와 시퀀스 관리 정책을 별도로 검토한다. 데이터 저장·조회 객체 항목 저장·동작 방식 용도 Tables 실제 행 저장 업무 데이터 Views SELECT 정의 저장 조회 재사용·노출 범위 관리 Materialized Views SELECT 정의와 결과 저장 무거운 집계 결과 재사용 Sequences 다음 번호 발급 IDENTITY·serial 등의 번호 생성 Foreign Tables 외부 데이터의 테이블 정의 FDW를 통한 원격 조회 View와 Materialized View View는 조회할 때 원본에 대한 쿼리가 실행되며, 해당 트랜잭션의 가시성 규칙에 따른 데이터를 읽는다. Materialized View는 이전에 저장한 결과를 읽는다. PostgreSQL 기본 Materialized View는 원본 변경마다 자동으로 갱신되지 않는다. REFRESH MATERIALIZED VIEW public.student_summary; Materialized View 자체에 인덱스를 만들 수 있다. 갱신 비용·잠금·허용 가능한 데이터 지연을 함께 설계해야 한다. Sequence 시퀀스는 동시 요청에서 번호를 안전하게 발급하지만 롤백해도 발급 번호를 되돌리지 않을 수 있다. 캐시와 실패 때문에 빈 번호가 생길 수 있으므로 회계 문서 등 무결번 업무 번호와 구분한다. 발급 순서가 커밋 순서와 같다는 보장도 없다. FDW의 구성 객체 역할 Foreign Data Wrapper 외부 접근 구현 Foreign Server 원격 접속 대상과 옵션 User Mapping 로컬 사용자와 원격 인증 연결 Foreign Table 외부 데이터 컬럼 구조·매핑 Foreign Table은 자동 복제본이 아니다. 외부 접근 시 네트워크·원격 실행·조건 pushdown 여부가 성능에 영향을 준다. 로직과 연산 객체 항목 역할 Functions SQL 식에서 호출할 수 있는 함수. 값·행 집합 반환과 데이터 처리 Procedures CALL로 실행하는 작업 단위 Trigger Functions 트리거가 호출하는 실행 코드 Aggregates 여러 행을 상태에 누적하여 집계 결과 계산 Operators 타입별 기호 연산과 처리 함수 연결 비교 Function Procedure 호출 SELECT f(...) 등 CALL p(...) 반환 값·여러 행·void 등 출력 매개변수 사용 가능 데이터 변경 가능 가능 내부 COMMIT/ROLLBACK 불가 호출 문맥·정의 옵션 등 허용 조건에서 가능 조회와 수정으로 둘을 구분하면 부정확하다. Function도 데이터를 수정할 수 있다. Trigger와 Trigger Function Trigger: 어느 객체의 어떤 이벤트에서, 언제·어떤 단위로 실행할지 정의. Trigger Function: 실행할 코드 정의. 행 트리거에서 OLD·NEW 등을 사용. 일반적인 DML 트리거는 해당 DML과 같은 트랜잭션에서 실행된다. 비동기 큐가 아니다. CREATE TABLE public.student_demo ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE FUNCTION public.set_student_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at := CURRENT_TIMESTAMP; RETURN NEW; END; $$; CREATE TRIGGER student_set_updated_at BEFORE UPDATE ON public.student_demo FOR EACH ROW EXECUTE FUNCTION public.set_student_updated_at(); 함수는 Trigger Functions, 연결 정의는 테이블 아래 Triggers에 해당한다. CURRENT_TIMESTAMP는 트랜잭션 시작 시각이므로 같은 트랜잭션 내 여러 갱신에 같은 값이 들어갈 수 있다. DDL에 반응하는 Event Trigger와 구분한다. 타입과 비교 규칙 항목 역할 예 Types 사용자 정의 타입 ENUM·복합 타입 Domains 기존 타입에 재사용 가능한 제약 추가 0~100 점수 Collations 문자열 정렬·비교 규칙 한국어 순서·대소문자 무시 CREATE TYPE public.student_status AS ENUM ('active', 'graduated', 'suspended'); CREATE DOMAIN public.score_100 AS integer CHECK (VALUE BETWEEN 0 AND 100); 위 Domain의 CHECK만으로 NULL이 금지되지는 않는다. 필수 컬럼에는 NOT NULL을 지정한다. ENUM 순서는 단순한 문자열 사전순이 아니라 정의된 ENUM 순서다. FTS: 전문 검색 객체 항목 역할 FTS Parsers 텍스트를 토큰으로 분리하고 종류 식별 FTS Dictionaries 토큰을 검색어로 정규화하거나 제거 FTS Templates Dictionary 처리 알고리즘의 구현 기반 FTS Configurations Parser와 토큰 종류별 Dictionary 연결 SELECT to_tsvector('english', 'The students are studying'); 영어 설정에서는 불용어 제거·어간 처리 등이 적용된다. tsvector는 전문 검색의 어휘·위치 표현이며 임베딩 벡터가 아니다. GIN으로 tsvector 검색을 가속할 수 있으나, Collation을 한국어로 설정한다고 한국어 형태소 분석기가 자동으로 생기는 것은 아니다. 3. SQL을 실행하는 프로세스 학습 자료의 개념도 — 캐시와 작업 공간의 차이. 세부 조건은 본문 설명을 함께 읽는다. 누가 실행하고, 누가 저장할까? 프로세스는 병행 동작한다. Parser·Planner·Executor는 Backend 내부 단계다. 프로세스 담당 역할 Postmaster 연결 수락, Backend 생성, 프로세스 감시 Client Backend 연결별 SQL 실행·트랜잭션·잠금 처리 Background Writer 버퍼 교체 부담을 줄이도록 dirty page를 미리 기록 Checkpointer 체크포인트에 필요한 페이지 기록과 동기화 WAL Writer WAL을 주기적으로 기록·flush Autovacuum Launcher / Worker 작업 스케줄링 / 실제 VACUUM·ANALYZE Parallel Worker 병렬 실행 계획의 일부 수행 Archiver 완료된 WAL 세그먼트 아카이빙 WAL Sender / Receiver 물리 복제 WAL 전송 / 수신 Startup Process 시작 시 복구 및 Standby의 WAL 재실행 Logger logging collector 활성화 시 로그 수집 연결 수락 → Backend 생성·인증 → SQL 수신 → Parse/Analyze → Rewrite → Plan → Execute → 결과 반환. Parser·Planner·Executor는 일반적으로 Backend 내부 단계이며 각각 별도 OS 프로세스가 아니다. 실제 서버 연결 하나에 Backend 하나가 대응하고, 유지되는 연결에서 여러 SQL을 처리한다. 읽기와 쓰기 — Backend도 데이터 페이지와 WAL을 쓸 수 있다. 모든 쓰기를 백그라운드에 넘기지 않는다. 추가 프로세스 — Parallel Worker는 병렬 실행, Archiver는 WAL 보관, Logger는 서버 로그 수집을 맡는다. 물리 복제 — Primary의 WAL Sender → Standby의 WAL Receiver → Startup Process의 WAL 재실행. 기억할 문장: 연결 하나 ≈ Backend 하나. 쿼리마다 새 프로세스를 만드는 것은 아니다. 4. 캐시와 작업 공간의 차이 캐시와 작업 공간은 다르다 Shared Buffers·OS Page Cache는 재사용, work_mem은 연산을 위한 메모리다. 구분 Shared Buffers OS Page Cache work_mem 주체 PostgreSQL OS PostgreSQL 실행기 대상 테이블·인덱스 페이지 파일 데이터 정렬·해시 중간 데이터 범위 인스턴스 공유 OS 파일 캐시 일반적으로 연산·프로세스별 크기 시작 시 설정 동적 변화 필요할 때 사용 부족할 때 페이지 교체 저장장치 읽기 증가 spill 가능 work_mem 은 연결당 예약값도 쿼리 전체 메모리의 절대 상한도 아니다. 일반적인 해시 예산은 work_mem × hash_mem_multiplier 이다. Shared Buffers와 OS 캐시에는 같은 데이터가 중복될 수 있다. 공유 캐시 — shared_buffers는 연결 수만큼 곱하지 않는다. OS 캐시와 같은 데이터가 중복될 수 있다. 작업 메모리 — 여러 연산·동시 쿼리·병렬 Worker가 메모리를 사용한다. hash_mem_multiplier도 고려한다. 설정의 함정 — effective_cache_size는 플래너 추정치이며 메모리를 할당하지 않는다. 기억할 문장: PgBouncer는 동시성을, work_mem은 연산별 메모리 예산을 조절한다. 5. 실습에서 먼저 확인할 것 SELECT version(), current_database(), current_user; SHOW search_path; SHOW shared_buffers; SHOW work_mem; SHOW hash_mem_multiplier; SHOW effective_cache_size; 연결 이름과 실제 버전을 혼동하지 않고, 공유 캐시의 크기와 연산별 메모리 예산을 구분해서 읽는다. 이 명령은 설정을 변경하지 않는다. 캐시 hit가 높다는 사실만으로 느린 SQL의 모든 원인이 해결되었다고 판단할 수는 없다. 자료 기준과 참고 문서 개인 PostgreSQL 학습 노트를 바탕으로 정리했다. 첨부 그림은 제공된 학습 자료를 사용했으며, 버전이나 설정에 따른 조건은 본문에 덧붙였다. PostgreSQL 구조 스키마 메모리와 프로세스 자원 시스템 카탈로그 이어서 읽기 · 다음 편 → · 전체 시리즈 목차 시리즈 목차 PostgreSQL의 전체 지도: pgAdmin·프로세스·메모리 UPDATE와 COMMIT의 저장 흐름: WAL·Checkpoint MVCC와 HOT: 한 행의 여러 버전은 어떻게 연결될까? VACUUM과 TXID 프리징: 공간과 오래된 트랜잭션 관리 대용량 테이블 읽기 줄이기: B-tree·BRIN·파티셔닝 GIN·GiST와 JSONB: 포함 조건과 인덱스의 쓰기 비용 같은 한글인데 왜 다를까? Collation·NFC·NFD PostgreSQL 텍스트 검색: FTS·pg_trgm·BM25 구분하기 ORM과 연결 관리: Flush·Commit·PgBouncer 느린 SQL 조사하기: EXPLAIN·pg_stat_statements·auto_explain pgvector의 HNSW·IVFFlat: 정확도와 탐색 비용 업무 DB에서 RAG까지: PGMQ·문서 버전·하이브리드 검색
Балл: 54.35Уверенность: 49%
Балл: 54.35Уверенность: 49%
Балл: 54.35Уверенность: 49%
Балл: 54.35Уверенность: 49%
Балл: 54.35Уверенность: 49%