본문으로 건너뛰기
1.6 TOAST와 큰 값 처리

1.6 TOAST와 큰 값 처리

PostgreSQL의 페이지는 8KB로 고정돼 있습니다. 그런데 컬럼 한 개의 값이 8KB를 넘는 경우는 흔하다 — 긴 텍스트, JSONB, bytea, 큰 배열. PostgreSQL은 이런 값을 자동으로 압축하거나 외부에 따로 저장해서 한 row가 한 페이지에 들어가도록 만듭니다. 이 기법의 이름이 TOAST(The Oversized-Attribute Storage Technique)다.

동작 시점

TOAST가 작동하는 기준은 tuple 한 개의 크기가 약 2KB(TOAST_TUPLE_THRESHOLD)를 넘을 때다. PostgreSQL은 다음 순서로 시도합니다.

    flowchart TD
  START["tuple 크기 측정"]
  CHECK1{"> 2KB?"}
  COMP["가장 큰 컬럼<br/>압축"]
  CHECK2{"여전히<br/>> 2KB?"}
  OUT["가장 큰 컬럼<br/>TOAST 테이블로 이동"]
  CHECK3{"여전히<br/>> 2KB?"}
  DONE["완료 — 디스크 쓰기"]

  START --> CHECK1
  CHECK1 -- "아니오" --> DONE
  CHECK1 -- "예" --> COMP
  COMP --> CHECK2
  CHECK2 -- "아니오" --> DONE
  CHECK2 -- "예" --> OUT
  OUT --> CHECK3
  CHECK3 -- "예" --> COMP
  CHECK3 -- "아니오" --> DONE

  classDef ok fill:#d1fae5,stroke:#047857,color:#064e3b
  classDef warn fill:#fed7aa,stroke:#c2410c,color:#7c2d12
  classDef proc fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
  class DONE ok
  class COMP,OUT proc
  class CHECK1,CHECK2,CHECK3 warn
  

핵심 원칙:

  1. 가장 큰 가변 길이 컬럼부터 차례로 처리
  2. 먼저 압축을 시도, 부족하면 TOAST 테이블에 외부 저장
  3. 외부 저장된 컬럼은 메인 row에는 작은 포인터(약 18B)만 남는다

저장 전략 (storage strategy)

각 컬럼은 4가지 storage 모드 중 하나를 갖습니다. ALTER TABLE ... ALTER COLUMN ... SET STORAGE로 변경 가능합니다.

모드압축외부 저장기본 적용 대상
PLAIN안 함안 함고정 길이 타입(int, timestamp 등)
EXTENDED시도시도대부분의 가변 길이 타입(text, jsonb, bytea …) — 기본값
EXTERNAL안 함시도자주 substring·정렬되는 큰 텍스트에 유리 (압축 없음 → 디코딩 없음)
MAIN시도마지막 수단인라인 압축은 받되 외부 이동은 피하고 싶을 때
-- 압축은 하되 외부 저장은 피함
ALTER TABLE articles ALTER COLUMN body SET STORAGE MAIN;

-- 외부 저장은 하되 압축은 안 함 (substring 자주 쓰는 큰 텍스트)
ALTER TABLE blobs ALTER COLUMN payload SET STORAGE EXTERNAL;

TOAST 테이블

외부 저장 대상이 되면 별도 TOAST 테이블에 옮겨집니다. TOAST 테이블은 사용자가 보지 못하게 pg_toast 스키마 안에 자동 생성됩니다.

SELECT relname, reltoastrelid::regclass
  FROM pg_class
 WHERE relname = 'articles';
-- relname  | reltoastrelid
-- ---------+---------------------------
-- articles | pg_toast.pg_toast_16395

TOAST 테이블의 컬럼 구조:

컬럼의미
chunk_id원본 값 한 개에 부여된 OID
chunk_seq청크 순서 (0부터)
chunk_data약 2000B의 데이터 청크

큰 값은 chunk_id를 공유하는 여러 청크로 쪼개집니다. 메인 row에는 chunk_id + 길이 정보가 담긴 작은 포인터만 남습니다.

    flowchart TD
  MainRow["articles 페이지<br/>id=42, title='…'<br/>body=<b>TOAST 포인터</b>"]
  ToastTable["pg_toast.pg_toast_16395<br/>(chunk_id, chunk_seq, chunk_data)"]
  MainRow -- "chunk_id = 99001" --> ToastTable
  ToastTable -. "chunk_seq 0,1,2,…" .- ToastTable

  classDef main fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
  classDef toast fill:#fed7aa,stroke:#c2410c,color:#7c2d12
  class MainRow main
  class ToastTable toast
  

압축 알고리즘

PostgreSQL 14부터 컬럼 단위 압축 알고리즘 선택이 가능합니다.

알고리즘도입특징
PGLZ항상 (전통 기본)LZ77 계열. 안정적이지만 압축률·속도 평범
LZ4PG 14+더 빠른 압축·해제, 비슷한 압축률. CPU 효율 우수

설정:

-- 클러스터 기본
ALTER SYSTEM SET default_toast_compression = 'lz4';
SELECT pg_reload_conf();

-- 컬럼 단위
ALTER TABLE articles ALTER COLUMN body SET COMPRESSION lz4;

기존 값은 그대로 PGLZ로 남습니다. VACUUM FULL 또는 pg_repack으로 재구성하면 새 알고리즘으로 재압축됩니다.

LZ4를 쓸 수 있는지 확인: PostgreSQL을 LZ4 지원으로 빌드해야 합니다. PGDG 패키지·공식 Docker 이미지는 기본 활성화합니다. SELECT 'x'::text::"char"; 정도가 아니라 SHOW default_toast_compression;lz4로 잡히는지 확인하는 게 빠릅니다.

TOAST 한도

한도
컬럼 한 개 최대 크기1GB (varlena의 길이 필드가 32-bit)
한 row(외부 저장 포함)의 논리 크기거의 제한 없음 (각 컬럼이 1GB까지)
단, 하나의 페이지에 인라인으로 들어가는 한도BLCKSZ / 4 = 2KB (TOAST_TUPLE_THRESHOLD)

따라서 1GB JSONB나 텍스트를 컬럼에 넣는 것 자체는 가능하지만, 실제로는:

  • 그렇게 큰 값은 매번 압축·해제·청크 결합 비용을 치름
  • 인덱싱·정렬·통계가 제대로 동작하지 않음
  • 운영상 1MB를 넘는 컬럼이면 별도 객체 저장소(S3·NCP Object Storage)로 빼는 게 표준

운영 시 보이는 흔적

큰 값이 들어간 테이블 확인

-- 테이블 본체 크기 vs TOAST 크기
SELECT
  relname,
  pg_size_pretty(pg_relation_size(oid))            AS main,
  pg_size_pretty(pg_relation_size(reltoastrelid))  AS toast,
  pg_size_pretty(pg_total_relation_size(oid))      AS total
  FROM pg_class
 WHERE relkind = 'r' AND reltoastrelid <> 0
 ORDER BY pg_total_relation_size(oid) DESC
 LIMIT 5;

main보다 toast가 더 크면 그 테이블은 본문보다 큰 값(긴 텍스트·JSONB)이 더 많은 디스크를 쓰는 상태입니다.

TOAST 컬럼의 압축 확인

SELECT attname, atttypid::regtype, attstorage, attcompression
  FROM pg_attribute
 WHERE attrelid = 'articles'::regclass AND attnum > 0;
-- attstorage: p(plain) e(extended) x(external) m(main)
-- attcompression: p(pglz) l(lz4) ''(default)

attcompression이 빈 문자열이면 클러스터 기본(default_toast_compression)을 따릅니다.

안티패턴

패턴문제권장
1GB 가까운 JSONB를 자주 UPDATEUPDATE는 새 tuple 작성 + 새 TOAST 청크 작성 → I/O 폭증자주 바뀌는 필드는 별 컬럼으로 분리
모든 메타데이터를 JSONB 한 컬럼에 넣기인덱싱·통계가 부정확, TOAST overhead 큼핵심 키는 별 컬럼, 나머지만 JSONB
큰 BLOB을 DB에 넣기TOAST는 효율적이지만 백업 크기·복제 지연을 키움외부 객체 저장소 + 참조 URL/키만 DB에
EXTERNAL 모드 디폴트로 전환압축 효과 상실, 디스크 사용량 증가substring 부하가 명백한 컬럼에만 한정
TOAST 청크가 손상되면 메인 row를 읽는 SELECT가 missing chunk number X for toast value Y 오류를 던집니다. 백업 전략에서는 pg_toast.* 테이블도 항상 같이 포함되어야 한다 (pg_dump·pg_basebackup은 자동으로 포함).

정리

  • TOAST는 tuple이 2KB를 넘을 때 자동으로 압축 → 외부 저장 순서로 처리
  • 메인 row에는 작은 포인터(약 18B)만 남고 실제 데이터는 pg_toast.pg_toast_<oid> 테이블에 청크로 저장
  • 컬럼별 storage 모드(PLAIN/EXTENDED/EXTERNAL/MAIN)로 동작 조절 가능
  • PG 14부터 LZ4 압축 옵션 — 일반적으로 PGLZ보다 빠름
  • 컬럼 1GB까지 가능하지만 운영상 1MB 초과 값은 객체 저장소로 분리하는 게 정석
  • 큰 JSONB의 잦은 UPDATE는 TOAST I/O 폭증을 부르는 대표 안티패턴

Part I의 PostgreSQL 아키텍처 개요가 끝났습니다. 다음 Part II에서는 위 구조를 가진 클러스터를 실제로 설치하고 초기화하는 방법 — Linux 패키지, 소스 빌드, 컨테이너·Kubernetes — 을 봅니다.