로그 테이블이 수억 행일 때, 파티셔닝이 정말 이득인지 따져봅니다

결론부터 말씀드리면, 로그 테이블이 수억 행 규모로 커졌다고 해서 파티셔닝부터 꺼내 드는 게 항상 정답은 아닙니다. 조회 조건이 거의 매번 날짜 범위로 걸리고, 오래된 로그를 주기적으로 지워야 하는 구조라면 파티셔닝이 확실한 효과를 냅니다. 반대로 전체 기간을 가로지르는 집계 쿼리가 많거나 파티션 관리를 자동화할 여력이 없다면, 이득보다 운영 부담이 먼저 커질 수 있습니다. 수억 행 로그 … Read more

JSONB에 다 넣기 전에 점검할 GIN 인덱스·TOAST 갈림길

테이블 설계를 할 때 속성을 JSONB에 다 넣기로 정하면 당장은 편합니다. 하지만 조회량이 늘어나는 순간부터 정규 컬럼과 체감 성능 차이가 벌어지기 시작합니다. 자주 필터링하거나 정렬하는 값을 JSONB 안에 묻어두면 GIN 인덱스가 생각보다 커지고, 값 하나가 조금만 길어져도 TOAST 테이블로 분리 저장되면서 읽기 비용이 따라붙습니다. 이 글은 PostgreSQL 12 이상, 기본 페이지 크기(8KB) 환경을 기준으로 정규 … Read more

SQLite 파일을 cp로 백업하면 깨지는 이유, VACUUM INTO와 백업 API로 해결하기

“SQLite는 파일 하나짜리 DB니까 cp 명령으로 복사만 해두면 백업이 끝난다”고 생각하는 분이 많습니다. 하지만 SQLite 파일을 cp로 백업하면 깨지는 경우가 실제로 있고, 그 이유는 cp가 SQLite 엔진의 잠금 상태나 WAL(Write-Ahead Log) 파일을 전혀 알지 못하기 때문입니다. 결과만 먼저 말씀드리면, 쓰기 작업이 전혀 없는 완전히 닫힌 DB를 복사할 때는 cp로도 문제가 없지만, 서비스가 돌아가는 중이거나 체크포인트가 … Read more

백업은 있는데 복구를 못 할 때, pg_dump와 PITR 중 무엇을 써야 할까요

백업은 있는데 복구를 못 하는 상황은 대부분 백업 방식과 복구 시간(RTO)을 미리 맞춰보지 않아서 생깁니다. pg_dump로 받은 백업은 논리적 스냅숏이라 특정 시점을 골라서 복구할 수 없고, PITR(Point-In-Time Recovery)은 원하는 시점을 고를 수 있는 대신 장애가 나기 전부터 WAL 아카이빙을 켜두지 않았다면 아예 쓸 수 없습니다. 이 글은 PostgreSQL 13 이상에서 recovery.signal / standby.signal 방식을 쓰는 … Read more

행을 지웠는데 디스크가 그대로인 이유, 데드 튜플과 오토배큠 튜닝으로 해결하기

DELETE 쿼리를 수십만 건 실행했는데 df -h로 확인한 디스크 사용량이 줄지 않아서 검색하셨다면, 결론은 간단합니다. PostgreSQL은 행을 지워도 그 공간을 바로 운영체제에 돌려주지 않습니다. 삭제된 행은 “데드 튜플”로 테이블 파일 안에 그대로 남아 있고, 이걸 치우는 오토배큠(autovacuum)이 제대로 돌지 않으면 테이블 파일 크기는 줄어들지 않습니다. 이 글은 PostgreSQL 13 이상, 단일 테이블 기준 autovacuum 파라미터 … Read more

기본키를 UUID로 했더니 느려질 때 — UUIDv4·UUIDv7·BIGSERIAL 비교

기본키를 UUID로 했더니 인서트가 느려질 수 있다는 이야기, 실제로 겪어보면 사실일까요? 조건만 맞으면 사실입니다. 완전히 무작위로 생성되는 UUIDv4를 기본키로 쓰면 B-Tree 인덱스에 값이 뒤죽박죽 꽂히면서 페이지 분할과 캐시 미스가 늘어나고, 테이블이 수백만 행을 넘기는 시점부터 그 차이가 체감될 만큼 커집니다. 이 문제는 시간 순서를 담은 UUIDv7이나 애초에 정수를 쓰는 BIGSERIAL로 기본키를 바꾸면 상당 부분 줄어듭니다. … Read more

인덱스를 만들었는데 Seq Scan이 뜨는 이유, EXPLAIN으로 좁히기

인덱스를 만들었는데 Seq Scan이 그대로 뜨는 상황 때문에 이 글을 검색하셨다면, 답은 대부분 “인덱스가 안 걸려서”가 아니라 “플래너가 비용 계산상 인덱스 쪽이 더 비싸다고 판단해서”입니다. PostgreSQL의 쿼리 플래너는 인덱스 존재 여부와 상관없이 통계 정보, 테이블 크기, 선택도를 종합해 더 저렴한 경로를 고릅니다. 그래서 인덱스를 지우는 것보다 EXPLAIN으로 플래너가 어떤 수치를 보고 있는지 먼저 확인하는 쪽이 … Read more

커넥션 풀 크기를 몇으로 둘까 — pool_size·워커 수 계산법

커넥션 풀 크기를 정할 때 “일단 넉넉하게 잡아두면 안전하다”고 생각하는 경우가 많습니다. 하지만 코어 4개짜리 서버라면 HikariCP가 제시하는 공식으로 계산했을 때 적정 pool_size는 9 정도에 불과합니다. 100, 200씩 크게 잡아둔 풀이 오히려 DB 서버의 컨텍스트 스위칭 부하를 키우고 응답 속도를 떨어뜨리는 경우가 흔합니다. 이 글에서는 pool_size·워커 수·max_connections를 어떤 순서로 맞춰야 하는지, 실제 공식과 숫자로 짚어보겠습니다. … Read more

운영 중 컬럼 추가가 테이블을 잠그는 이유와 피하는 마이그레이션 순서

많은 분들이 ALTER TABLE ADD COLUMN을 가벼운 메타데이터 작업이라고 생각하시는데, 실제로는 운영 중 컬럼 추가가 테이블 전체를 잠그는 사고로 이어지는 경우가 많습니다. DEFAULT 값의 종류, NOT NULL 제약조건 유무, DB 엔진과 버전에 따라 잠금 범위와 지속 시간이 완전히 달라지기 때문입니다. 서비스 중단을 피하려면 nullable 컬럼을 먼저 추가하고, 데이터를 배치로 채운 다음, 제약조건은 검증 단계를 분리해서 … Read more

크롤러가 조용히 0건을 내놓을 때, 진짜 실패인지 판단하는 기준

크롤러를 운영하다 보면 “에러 로그가 없으니 정상 작동했다”고 믿는 경우가 많습니다. 하지만 크롤러가 조용히 0건을 내놓는 상황은 에러 한 줄 남기지 않고도 자주 일어납니다. 0이라는 숫자 자체만 보고 성공과 실패를 가르면 안 되고, 상태 코드·응답 크기·과거 수집량 같은 주변 신호를 같이 봐야 실패인지 정상인지 구분할 수 있습니다. 0건이 나오는 세 가지 서로 다른 상황 같은 … Read more