PostgreSQL 심화 — 트랜잭션·인덱스·JSONB·파티션 · JSONB·파티션·유지보수 · 이론
JSONB·파티션·유지보수 — 대용량과 문서를 다룬다
한 줄 요약
jsonb 는 스키마 없이 문서를 담되 인덱스와 생성 열로 관계형처럼 다룰 수 있고, 파티션은 큰 표를 시간으로 쪼개 가지치기로 빠르게 만든다. 그리고 갱신이 쌓이면 표가 부푸는데, 그 회수를 아는 것이 유지보수의 절반이다.
왜 이게 필요했나
주문을 통째로 jsonb 한 열에 담았다. 유연해서 좋았는데, 이제 "gift 태그가 붙은 주문 수" 를 세려니 어떻게 꺼내는지 모르겠다. 센서 로그 표는 1억 행이 넘어 인덱스도 안 듣고, 오래된 달을 지우려면 하루가 걸린다. 한 컬럼만 바꿨는데 표 크기가 두 배가 됐다.
이 셋은 각각 jsonb 질의, 파티션, 그리고 MVCC 부풀림의 문제다. 셋 다 개발자가 매주 만나지만, 도구를 모르면 매번 처음처럼 헤맨다.
어떻게 동작하나
jsonb 는 연산자로 다룬다. -> 는 jsonb 를, ->> 는 텍스트를 꺼내고, @> 는 담김(부분 포함)을, ? 는 키/배열 원소 존재를 본다. 자주 꺼내 쓰는 값은 생성 열(generated always as (doc->>'x') stored)로 뽑아 두면 평범한 열처럼 인덱스하고 집계할 수 있다.
파티션은 큰 표를 나눈다. PARTITION BY RANGE (ts) 로 월별 파티션을 만들고, 범위 밖 행은 DEFAULT 파티션이 받는다. ts 로 거르면 옵티마이저가 관계 없는 파티션을 계획에서 아예 뺀다 — 이것이 가지치기다. 오래된 달은 파티션을 통째로 떼면(DETACH) 되고, DEFAULT 에 쌓인 데이터는 나중에 전용 파티션으로 붙일(ATTACH) 수 있다.
갱신은 표를 부풀린다. PostgreSQL 은 행을 고치지 않고 새 버전을 쓴 뒤 옛 버전에 죽음 표시를 남긴다. 그 죽은 튜플을 VACUUM 이 회수한다. pg_stat_user_tables 의 n_dead_tup 이 그 양을 보여 준다.
무엇이 느린지는 재봐야 안다
어떤 질의가 서버 시간을 먹는지는 짐작하면 틀린다. pg_stat_statements 가 질의별 호출 수와 총 실행 시간을 쌓아 준다. 이 확장은 공유 라이브러리라 shared_preload_libraries 에 넣고 서버를 재시작해야 켜진다. 켜 두면 "상위 질의" 목록이 튜닝의 출발점이 된다.
실무에서 진짜 중요한 것
첫째, jsonb 에도 인덱스를 건다. 문서라고 순차 스캔을 감수할 이유는 없다. 담김 질의에는 GIN 을, 자주 거르는 스칼라 값에는 생성 열 + B 트리를 쓴다.
둘째, 큰 시계열은 처음부터 파티션한다. 나중에 쪼개는 것은 이관 작업이 되지만, 파티션 위에서는 오래된 달을 떼는 것이 메타데이터 한 번이다. 파티션 키는 거의 항상 질의가 거르는 시간 열이다.
셋째, 부풀림은 원인을 표 밖에서도 찾는다. n_dead_tup 이 큰데 VACUUM 이 회수하지 못하면, 아직 열려 있는 오래된 트랜잭션이 그 옛 버전을 볼지도 모르기 때문이다. 다음 실습에서 이 셋을 직접 만들고 확인한다.