모든 태그

# PostgreSQL

29개의 글

여럿이 쓰는 관제탑: 팀 경계와 멀티노드, 그리고 호스트 차원

이기종 DBMS 운영 관리 플랫폼 DBTower의 멀티테넌시와 호스트 차원 기록입니다. 앞부분은 여러 팀이 한 콘솔을 쓰기 시작하는 국면을 다룹니다. 팀 사용자는 자기 팀 인스턴스와 전역만 보고, 남의 팀 인스턴스는 id로 직접 찔러도 403이 아니라 404를 받습니다(존재 자체를 숨김). 강제 지점은 단 한 곳(RegistryService)입니다. 세션을 메타 DB로 옮겨 재시작에도 로그인이 살아남게 했고, 그 과정에서 Boot 자동구성이 인메모리로 조용히 폴백하는 함정을 밟았습니다. 이렇게 준비한 노드를 실제로 늘렸더니, 분산 락 하나 때문에 두 번째 노드가 놀고 있었습니다. 샤드별 락으로 바꾸니 두 노드가 수집을 나눠 들고, 한 노드를 죽이면 남은 노드가 설정 변경 없이 전 샤드를 인수하며, 같은 쿠키로 로그인도 유지됩니다. 로그인 잠금 카운터도 메타 DB로 옮겨, 노드 A에서 두 번·B에서 한 번 틀리자 네 번째가 잠기는 것을 실측했습니다. 최대 볼륨 테이블은 월별로 파티셔닝해 보존 정리를 DELETE 1.9초에서 DROP 12.8ms로 줄이고 블로트를 아예 없앴으며, 커넥션은 온디맨드로 바꿔 격리 대상의 유휴 커넥션을 1개 영구에서 0으로 만들었습니다. 뒷부분은 호스트 차원입니다. 디스크 포화 예측은 잔량이 아니라 속도를 봅니다. 여유가 76.8%나 남았는데 치명 경보가 뜨는 화면을 실쓰기 부하로 직접 만들었습니다. 초당 17MB씩 줄고 있으면 20시간 뒤 장애이므로 이 경보가 맞습니다. 여기서도 node-exporter가 rootfs 마운트 없이 컨테이너 자기 자신만 보고 있던 함정과, mountpoint="/" 고정이 데이터 전용 마운트를 쓰는 실무와 어긋나는 설계 함정을 만났습니다. 마지막은 서버 공유 인지입니다. 등록 단위는 DB인데 물리 단위는 서버라, 같은 서버에 DB 두 개를 등록하면 세션·복제·데드락 경보가 두 번 울립니다. 이를 그룹당 1회로 줄이되 "누구에게 해당하는지"를 명시하고, 헬스 스코어(위험 귀속)는 일부러 dedup하지 않은 선 긋기를 기록했습니다.

아무도 못 쓰던 프로젝트를 셀프호스트 제품으로 끌어올리고, 화면 패리티까지 맞추다

이기종 DBMS 운영 관리 플랫폼 DBTower 프로덕션화·화면 패리티 편. 기능은 레퍼런스로 삼은 사례를 넘어섰는데, 정작 "남이 클론해서 실제로 쓸 수 있나"를 물으니 답이 아니었습니다. 블로커는 넷이었습니다. 라이선스가 없어 법적으로 아무도 못 썼습니다. 암호화 fail-closed가 하필 셀프호스트가 쓰는 docker 프로필만 비껴가, 대상 DB 비밀번호가 평문으로 저장됐습니다. 비밀번호 컬럼이 든 옛 H2 파일은 커밋된 채였고, AI 판단 규칙 파일은 이미지에서 빠져 빈 프롬프트로 돌고 있었습니다. Phase 0(배포 블로커) 넷을 없애고, 관제탑이 자기 자신을 지키기 시작하고(로그인 잠금·메타 백업·웹 HTTPS), 문의에 참조 테이블 스키마를 붙였습니다. 이어서 레퍼런스 발표의 화면 11장을 컬럼 단위로 전수 대조하며 표 컬럼 패리티를 맞췄고, 그 과정에서 함정 셋을 만났습니다. 단위 테스트 382건이 초록인데도 웹 콘솔 전체가 백화된 채 커밋돼 있었습니다. "카탈로그 재구성(근사)" 배지는 알고 보니 게으름의 라벨이었습니다. CPU 그래프를 붙이다 보니 활동 그래프가 9시간 미래의 빈 구간을 조회하고 있었습니다. 전부 라이브 실측과 함께 기록합니다.

백업이 진짜가 되기까지: 로그 백업 5기종과 시점 복구, 그리고 정석까지의 네 걸음

이기종 DBMS 운영 관리 플랫폼 DBTower 6편. 백업 대장정입니다. 전반부는 로그 백업이 MSSQL만 되던 것을 MySQL binlog, PostgreSQL WAL, MongoDB oplog, Oracle 아카이브 로그까지 다섯 기종 전부로 넓히며 "기종이 못 하는 것"과 "하다가 깨진 것"을 구분하는 UNSUPPORTED 상태를 만들고, 최신 파일 하나만 수집하면 체인에 조용한 구멍이 난다는 것을 "마지막 이전 전부" 보충 수집으로 고치고, 생성한 복원 안내문을 실제로 실행해 SQL Server와 PostgreSQL에서 목표 시점의 상태를 정확히 재현한 기록입니다. 후반부는 그걸 현업의 정석 쪽으로 옮겨 가는 네 걸음입니다. Mongo oplog 증분($gte로 일부러 만든 겹침 한 건이 체인 무결의 증거, 산출물 28분의 1), pg_receivewal 스트리밍(복제 슬롯 덕에 수신자를 죽여도 재시작 사이 유실 0, 그리고 죽은 프로세스를 산 것처럼 보이게 하던 docker exec -i 함정), MySQL 물리 백업 XtraBackup(MYSQL_PWD를 안 읽고 /dev/stdin defaults를 조용히 무시하는 함정 두 겹, 검증은 파일 존재가 아니라 실제 prepare 실행), 그리고 AES-256-GCM 산출물 암호화(변조된 백업은 조용히 오염되는 대신 명확히 실패한다)까지입니다.

DBTower 포트폴리오 총정리: 이기종 DBMS 5기종을 인터페이스 하나로 관제·진단하기까지, 실측 117절

MySQL, PostgreSQL, SQL Server, Oracle, MongoDB를 하나의 관제탑에서 등록하고 진단하고 백업하고 자율 감시하는 컨트롤 플레인 DBTower의 포트폴리오 총정리입니다. 도구 파편화와 DBA 반복 문의라는 문제 정의에서 출발합니다. 추상화 경계를 SQL이 아니라 운영 행위에 그은 설계 결정과, 그 결정을 성격이 정반대인 두 기종을 실제로 추가해 코어 0줄 수정으로 증명한 과정을 담았습니다. 자기 자신을 관리 대상으로 등록해 자기 풀스캔을 잡은 도그푸딩(21.269ms에서 0.062ms), 따옴표 하나로 인덱스가 죽는 암시적 형변환을 추정 대 실제 괴리 300배로 지목하고 정합성 사고까지 증명한 심층 진단, FULL 앵커와 LOG 체인이 병행하는 정석 백업과 실제 시점 복구, 결함 20건 이상을 스스로 감사해 FIX와 SKIP을 가른 하드닝까지. 모든 성능 수치는 개선 전후를 직접 잰 실측이고, 재현 절차는 저장소 VERIFICATION.md 117개 절에 있습니다.

v1.0.0 이후, 다섯 기종을 더 깊이 판 심화 아크들과 내가 만든 걸 감사한 기록

이기종 DBMS 운영 관리 플랫폼 DBTower 심화 편. v1.0.0을 찍은 뒤 문서에 정직한 잔여로 남겨둔 것들을 다시 붙잡았습니다. 그중 셋을 닫았습니다. 쿼리도 데이터도 그대로인데 갑자기 느려지는 플랜 플립을 PostgreSQL 16의 GENERIC_PLAN으로 감지하고, 로컬 백업을 S3 호환 오프사이트로 올려 3-2-1을 채웠으며, TLS 강제 관리형 서비스에 붙되 인증서 검증 우회 옵션은 일부러 만들지 않았습니다. 여기서 심화 아크 넷으로 들어갑니다. 플랜 플립은 기종마다 다른 획득 경로를 shape 정규화 한 겹으로 통일해 다섯 기종으로 넓혔고, p95의 정직 등급은 누적에서 최근 구간으로, 미지원에서 추정으로 끌어올리되 못 올리는 Oracle은 라벨로 대비시켰습니다. 설정 변경 없이 세 기종에서 데드락을 읽었고, 관제가 부하가 되지 않도록 스케일을 다섯 축으로 제어했습니다. 끝으로 만든 것을 스스로 감사해, 동시성·정확성·보안·수명주기 네 축을 훑고 OWASP·CWE·벤더 문서와 대조해 FIX와 SKIP을 갈랐습니다.

아카이브가 자기 자신을 지우던 치명 경로를 막고, CI와 deadman과 dbt contracts로 신뢰를 마저 묶은 이야기

코드 감사에서 받은 결함 목록의 1번이 치명이었습니다. offload의 멱등 재적재는 '파티션을 통째로 지우고 다시 쓴다'인데 삭제가 원천 0행 체크보다 먼저라, 원천 보존(7일) 밖의 dt를 backfill이나 Clear로 재실행하면 아카이브 유일본 parquet를 지운 뒤 아무것도 안 쓰고 exit 0으로 '성공'합니다. 1부에서 이 경로를 실제로 재현하고 fail-closed 가드(ArchiveSelfDestructError, exit 1 → 재시도·webhook 경로 탑승)로 막았습니다. 같은 감사에서 나온 나머지 셋, 곧 게이트의 원천 Seq Scan(332ms/31k버퍼 → 인스턴스별 인덱스 루프 20ms/76버퍼), publish 혼합 버전(개별 커밋 → 단일 트랜잭션), 유지보수 DAG의 데모 테이블 하드 참조도 걷어내고 pytest 35개로 고정했습니다. 2부는 그 신뢰를 커밋·침묵·계약 세 축으로 마저 묶습니다. CI(GitHub Actions 3관문: ruff·pytest·dbt)가 임베디드 DuckDB 덕에 MinIO도 PG도 없는 러너에서 tiny 픽스처 parquet로 dbt build를 e2e로 돌리고(PASS=25), dbt unit test로 델타 로직 엣지 4개를 정적 입력→기대 출력으로 못박고, deadman heartbeat가 30h 침묵을 실제 경보 발화로 잡고(기한 26h), dbt contracts가 마트 컬럼 타입을 DB 레벨로 강제해 latency_increase_ms를 VARCHAR로 바꾸자 빌드가 'data type mismatch'로 막혔습니다. 회귀는 없었습니다. verify는 ALL MATCH(149,259/79,894행), pytest는 53개 통과입니다.

DB 내부 ⑪: SQL 실행기, 텍스트에서 행까지, 조인 3형제와 3값 논리

SQL은 '무엇을'만 말하는 선언형 언어다. 그 텍스트가 실제 행이 되기까지의 파이프라인을 해부한다. 렉서(maximal munch, 예약어는 왜 생기나), 재귀 하강 파서(문법 규칙 하나 = 함수 하나, 서브쿼리에서 파서가 자기 자신을 부른다, 손 파서 vs bison/Lemon의 갈림), 그리고 실행기에서 스트리밍과 materialize가 갈리는 이유. 본론은 조인 3형제로, 모든 조인의 출발점인 중첩 루프(이중 루프), 안쪽을 점 조회로 바꾸는 인덱스 NLJ, O(N+M)의 해시 조인, 그리고 안 만든 정렬 병합까지 선택 기준을 표로. GROUP BY가 왜 정렬(또는 해시)을 요구하는지(GroupAggregate vs HashAggregate), HAVING은 왜 그룹의 WHERE인지. 마지막으로 BETWEEN이 실행기 0줄로 끝난 이유(문법 설탕), LIKE의 백트래킹 two-pointer 매칭과 LIKE '%x%'가 B-tree를 못 타는 이유(트라이그램·역색인으로의 탈출구), uncorrelated 서브쿼리의 1회 실행 캐시, 그리고 NULL의 3값 논리(UNKNOWN, NOT IN + NULL 함정)까지, 시리즈의 실행기 축을 완결한다.

DB 내부 ⑨: 복제, 복구의 redo를 스트림으로, base backup + WAL 스트리밍까지

복제의 첫 통찰은 '복제가 거의 공짜'라는 것이다. no-force WAL이 이미 커밋의 순차 스트림이라, replica가 할 일은 크래시 복구의 redo를 '파괴적 일회성'이 아니라 '증분·연속'으로 돌리는 것뿐이다. PostgreSQL에서 walreceiver가 WAL을 받아 쓰고 startup process가 replay하는 그 구조. 이를 소켓에 올리면 walsender/walreceiver가 되는데, 스트림엔 경계가 없어 길이 프레이밍이 필요하고, 수신자는 읽기 전용 fd에 쓸 수 없어 실제로 교착을 밟았다. 마지막은 캡스톤, 곧 진짜 엔진의 커밋이 복제본에서 SELECT되기까지다. 코드보다 어려웠던 건 조용히 깨지는 landmine 셋: 재오픈이 WAL을 truncate하고 LSN을 리셋해 replica가 신규 커밋을 조용히 스킵하는 것, base 스냅샷에 steal된 미커밋 페이지가 딸려오는 것, 카탈로그의 낡은 next_txn이 복제된 행을 미커밋으로 보이게 하는 것. 셋을 정리하면 필연적으로 실제 시스템과 같은 모델, 곧 pg_basebackup + streaming replication에 도달한다. 구조가 그렇게 강제한다.

DB 내부 ⑦: 저장 엔진의 세 철학: 힙 vs 클러스터드 vs LSM, 그리고 USING lsm

같은 행들을 저장하는 세 가지 철학이 있다. PostgreSQL의 힙(순서 없이 쌓고 인덱스가 RID로 가리킴), InnoDB의 클러스터드(데이터 자체를 PK 순서로, 보조 인덱스는 PK를 들고 이중 조회), RocksDB의 LSM(제자리에서 절대 안 고침: memtable→SSTable→compaction). 한 코드베이스에 셋을 세워 실측하면 교과서의 문장들이 숫자가 된다: PK 점 조회 1.2배·범위 3.8배 클러스터드 우세(지역성), 보조 점 조회는 2배 열세(이중 조회), LSM은 쓰기가 순차화되는 대신 읽기가 여러 SSTable을 뒤진다(read amplification). 마지막은 이 시리즈의 캡스톤이다. LSM을 진짜 엔진의 PK 인덱스로 배선하며(CREATE TABLE ... USING lsm) 부딪힌 벽: MVCC 다중버전 때문에 인덱스는 비유니크 멀티맵이어야 하고, unique LSM으로는 담을 수 없어 dedup 단위를 (key,val)로 바꾼 멀티값 모드가 필요했다. 자체 WAL이 없어 트랜잭션 롤백에 못 끼는 이 LSM 인덱스를 'heap에서 재구축되는 파생 가속기'로 다루는 설계까지. MyRocks가 테이블 단위로 저장 엔진을 고르게 하듯, pluggable 엔진 API의 축소판이다.

Balruno MVP 후기

게임 밸런싱 스프레드시트 + 문서 워크스페이스 Balruno의 백엔드 설계와 운영을 한 글에 정리합니다. PostgreSQL JSONB 채택(50,000 시트 환경에서 MySQL/PG/Mongo 직접 측정 — Sheet GET p95: PG 16ms / MySQL 25ms / Mongo 45ms, Name UPDATE p95: Mongo 37ms / PG 40ms / MySQL 63ms. 쓰기만 보면 MongoDB가 조금 빨랐지만, 한정된 인프라 안에서 DB를 둘로 나누지 않고 하나로 운영하는 편이 더 합리적이라고 판단해 PostgreSQL 선택), 시트 셀 + 시트 트리 + 문서 트리 3영역 통합 동기화 알고리즘(Baserow + Linear + Outline 합본), OCI Always Free 4대 + Ansible 자동화 + Cloudflare R2 3-2-1 백업으로 매니지드 대비 예상 회피 비용 연 약 $1,860, OAuth-only + 자체 발급 JWT(Auth0 대비 연 약 $2,880), Grafana + Loki + Alloy + Prometheus + InfluxDB 셀프 호스트 모니터링(Datadog 대비 연 약 $720), nginx blue/green 무중단 배포(첫 cutover 21초 → 두 번째부터 0초), 시트 도메인 100% 서버 진실원 전환(약 80,000 라인 정리)까지 포함합니다.

DB 내부 ⑥: 비용 기반 옵티마이저, 플래너가 멍청해지는 순간을 통계로 고치기

'인덱스가 있으면 무조건 쓴다'는 규칙은 id > 100 앞에서 무너진다. 행마다 한 번씩 901번의 힙 페치가 7페이지 순차 스캔보다 백 배 비싸게 과금되기 때문이다. 비용 기반 최적화(CBO)의 세 재료를 순서대로 짓는다: ANALYZE(행 수·페이지 수·PK min/max를 재는 통계), 선택도(균등분포 가정으로 매칭 행 수 추정, 히스토그램이 필요한 이유까지), 비용 모델(순차 = 페이지 수, 인덱스 = 1 + 매칭 행수만큼의 힙 페치). 그러면 같은 PK 범위 조건이 매칭 양에 따라 점 조회/인덱스/순차로 갈리는 크로스오버가 실제로 나타난다. '인덱스를 걸었는데 왜 안 타요?'의 정답이다. 후반부는 다중 테이블의 진짜 고민, 조인 순서다: 순서 하나가 2.8배를 가르고, n!을 2ⁿ으로 줄이는 Selinger의 부분집합 DP(1979), 교차곱을 피하는 연결성 규칙, 조인 방법(인덱스 NLJ vs 해시)까지 한 번에 고르는 계획기를 짓는다. EXPLAIN이 실행기와 같은 결정 함수를 공유해 '플랜이 거짓말하지 않는' 원칙도 함께.

데이터베이스 인덱스 ⑥: 운영과 한계

인덱스 시리즈 마무리. 운영 환경의 인덱스 작업은 DDL이 DB를 멈출 수 있다는 사실에서 출발합니다. CREATE INDEX CONCURRENTLY의 4단계 phase와 제약, 장기 트랜잭션이 클러스터 전반의 VACUUM/IOS에 미치는 영향, 인덱스 bloat과 REINDEX, 수십억 행을 위한 파티셔닝/샤딩, Bloom Filter, 그리고 6가지 안티패턴까지 1차 자료 기준으로 정리합니다.

데이터베이스 인덱스 ③: Covering Index와 Index-Only Scan

plan에 Index Only Scan이 잡혔다고 진짜 IOS는 아닙니다. PostgreSQL의 IOS는 covering(쿼리 컬럼이 인덱스에)과 visibility(VM all-visible) 두 단계 조건을 모두 만족해야 Heap Fetches가 0이 됩니다. INCLUDE 절은 covering을, VACUUM은 visibility를 충족시키는 도구입니다. INCLUDE의 leaf-only 저장 메커니즘과 인덱스 타입별 IOS 지원, 그리고 PG12 이전 insert-only Mandrill 함정까지 다룹니다.

데이터베이스 인덱스 ①: 인덱스 기초와 EXPLAIN 읽기

인덱스는 검색용 보조 자료구조이고, 그 인덱스를 쓸지 말지를 결정하는 것은 옵티마이저입니다. 옵티마이저의 결정을 검증하는 도구가 EXPLAIN이고, 실측까지 더하는 것이 EXPLAIN ANALYZE입니다. cost가 임의 단위라는 점, 추정과 실측의 격차가 진단의 핵심 신호라는 점, 인덱스가 있어도 안 쓰이는 4가지 패턴까지 1차 자료 기준으로 정리합니다.

DB 내부 ⑤: 진짜 psql이 붙는 서버, PostgreSQL wire protocol 해부

DB 서버와 클라이언트 사이엔 '프로토콜이라는 계약'이 있다. 그 계약만 지키면 상대는 내가 진짜 PostgreSQL인지 구분하지 못한다. PostgreSQL wire protocol v3를 바이트로 뜯는다: 유일한 변칙인 startup 핸드셰이크와 SSLRequest 거절, [타입 1B][길이 4B][페이로드] 프레임, RowDescription/DataRow/CommandComplete의 세 가지 응답 모양, 그리고 psql 프롬프트(=> / =*> / =!>)를 바꾸는 ReadyForQuery 상태 바이트 한 개까지. 400줄 서버로 실제 psql 14가 접속했고, 좋은 계층 분리 덕에 SQL 실행기는 한 줄도 안 바꿨다. 클라이맥스는 두 psql 터미널이다. 한쪽이 미커밋 UPDATE를 쥔 채로 다른 쪽이 옛 버전을 막힘 없이 읽는, MVCC의 존재 이유가 네트워크 너머에서 증명되는 장면이다. 커넥션당 스레드와 그 뒤에 숨은 굵은 latch의 한계까지 정리한다.

DB 내부 ④: 격리, 2PL에서 MVCC 스냅샷 격리까지, reader가 writer를 안 막기까지

동시에 여럿이 읽고 쓸 때 DB는 어떻게 안전한가. 두 철학이 있다. 충돌을 미리 막는 2PL(잠금)과 버전을 갈라 충돌을 피하는 MVCC다. MVCC의 두뇌는 가시성 규칙 한 줄(xmin 커밋 AND xmax 미커밋이면 보인다)이고, 그 규칙이 성립하려면 DELETE조차 지우면 안 된다(xmax 도장). 그 순간 힙을 읽는 아홉 갈래 전부에 가시성 게이트가 필요해지고, '인덱스는 MVCC를 모른다'는 원칙이 선다. 일부러 만든 쓰레기(dead tuple)는 VACUUM이 치우는데, PostgreSQL nbtree처럼 병합 없는 lazy 삭제로 충분한 이유, VACUUM해도 파일이 안 줄어드는 이유까지 코드로 확인한다. 마지막으로 reader의 락을 제거하고 BEGIN 시점 스냅샷(PG의 xmax/xip 축소판)을 고정하면, 한 트랜잭션이 미커밋 UPDATE를 쥐고 있어도 다른 쪽이 옛 버전을 막힘 없이 읽는 진짜 스냅샷 격리가 선다. 그 여파로 PK 인덱스가 다중 버전 멀티맵이 되어야 했던 것까지, C 구현으로 확인하며 정리한다.

이기종 DBMS를 하나의 컨트롤 플레인으로: DBTower 설계와 쿼리 회귀 감지

MySQL·PostgreSQL·SQL Server처럼 서로 다른 DBMS를 등록부터 모니터링, 백업까지 한 곳에서 관리하는 컨트롤 플레인 DBTower의 설계편입니다. 기종마다 다른 통계 소스(performance_schema·pg_stat_statements·DMV)를 DbmsOperator 인터페이스 하나로 묶었고, 같은 '백업'이 mysqldump·pg_dump·BACKUP DATABASE로 갈리는 차이도 그 인터페이스 뒤로 감췄습니다. '부하 상위 쿼리가 곧 범인은 아니다'라는 문제의식에서 시점 비교가 나왔고, 플랫폼이 자기 자신을 등록해 병목을 잡은 도그푸딩에서는 Seq Scan을 21ms에서 0.06ms로 줄였습니다. 이 시점 비교를 사람이 아니라 플랫폼이 스스로 돌려 쿼리 회귀를 자동으로 감지하고 Discord로 알립니다. 이런 설계 결정과 실측 수치를 함께 기록했습니다.

DB 내부 ③: WAL과 크래시 복구, redo-only에서 steal + no-force까지

쓰다가 전원이 꺼지면 어떻게 되는가. WAL 프로토콜의 본질(데이터보다 로그 먼저, fsync 한 번이 내구성의 분기점)에서 시작해, 복구 규칙이 'redo 아니면 discard' 하나로 끝나는 가장 단순한 형태를 짓고, 그 단순함의 대가(버퍼 풀보다 큰 트랜잭션이 죽는다)에 부딪혀 steal을 켜고(before-image·undo의 필연), 마지막으로 no-force(커밋 = 로그 fsync 하나)로 옮겨 로그가 진실의 원천이 되기까지. STEAL/FORCE 사분면이 어떤 복구 로직을 필수로 만드는지, 진짜 ARIES(physiological 로깅·pageLSN·CLR·3-패스)와의 거리는 어디인지, fsync의 가격(같은 5천 행 적재가 23배 차이)은 얼마인지, 크래시를 실제로 주입한 테스트와 실측으로 확인하며 정리합니다.

DB 내부 ②: B+Tree 인덱스와 O(log n), 그리고 인덱스는 왜 단순 key→value가 아닌가

인덱스는 왜 이진 트리가 아니라 B+Tree인가. 답은 디스크다(노드=페이지, fan-out이 높이를 무너뜨린다). 가장 어려운 노드 분할(리프는 copy-up, 내부는 push-up), 범위 스캔을 공짜로 만드는 리프 사슬, 그리고 유일성을 내려놓는 순간 부딪히는 문제들, 곧 중복 키의 하한 탐색(>= 한 글자가 조용히 틀린 결과를 만든다)과 '인덱스는 후보일 뿐, 진실은 힙에 있다'는 재검사(recheck)까지. 인덱스 vs 풀 스캔을 실측하면 1천 행에서 11배, 10만 행에서 416배로 벌어진다. O(log n)과 O(n)의 모양 그 자체다. 해시·LSM과의 갈림길, PostgreSQL·InnoDB 대조를 C 구현으로 확인하며 정리한다.

DB 내부 ①: 저장의 뼈대, 페이지·슬롯·행 포맷·힙·버퍼 풀은 어떻게 맞물리는가

관계형 DB의 맨 아래는 다섯 겹이다. 고정 크기 페이지(왜 모든 DB가 블록 단위인가), 슬롯 페이지(가변 행 + 안정적 주소), 행 포맷(null 비트맵), 힙 파일(RID), 그리고 버퍼 풀(캐시인데 안 깨지는 이유). PostgreSQL 8KB·InnoDB 16KB가 왜 그 크기인지, TID/RID가 왜 인덱스의 기반인지, NULL은 왜 값이 아니라 비트로 저장되는지, pin 프로토콜이 어떻게 latch와 분업해 멀티스레드에서 페이지를 지키는지를, C로 미니 DB를 직접 구현하고 ThreadSanitizer로 계측하며 확인한 내용을 1차 자료 기준으로 정리합니다.

재전송의 이중 거래를 멱등키로 막고, 대조된 적 없던 원장을 EOD 대사로 맞추다

4편까지 원장은 거래를 3값(SUCCESS/FAILED/UNKNOWN)으로 적고 UNKNOWN을 수동으로 해소할 수 있었지만, 두 구멍이 남아 있었습니다. 하나, 게이트웨이는 호출자의 재전송을 구분하지 못합니다. 타임아웃(UNKNOWN)을 받은 호출자가 같은 요청을 다시 보내면 그건 새 거래고, 계정계에서 두 번 실행돼 이중 거래가 됩니다. 둘, 원장은 한 번도 계정계와 대조된 적이 없는 진실입니다. 5편은 둘을 채웁니다. 멱등키는 (키+메서드+경로)를 DB 유니크 제약으로 원자적 선점합니다. 동시 재요청은 앱 락으로 못 묶고 DB가 하나로 만듭니다. 처리 중 재요청은 409, 완료된 요청 재수신은 저장된 원응답을 재실행 없이 재반환합니다. 같은 키로 잔액조회를 두 번 보내니 계정계 로그에 요청 수신은 1회, 원장에 그 거래는 1행으로 남아 이중 거래 0을 psql로 확인했습니다. EOD 대사는 계정계 당일 처리내역(가변 전문)을 원장 전량과 거래고유번호로 대조해 양쪽일치 / 금액상이 / 우리만있음 / 저쪽만있음의 4유형으로 가립니다. UNKNOWN은 대조 전에 상태조회·망취소로 자동 해소합니다. 통제된 5건으로 {MATCH:2, MISMATCH:1, LEDGER_ONLY:1, CORE_ONLY:1, UNKNOWN_RESOLVED:1}을 실측했고, 여기서 순서 함정을 하나 발견했습니다. 자동 해소의 망취소가 계정계 기록도 바꾸므로, 계정계 스냅샷은 해소 이후에 떠야 합니다. 마지막으로 원장 PG를 DBTower 관제 대상으로 등록 가능하게 준비했습니다.