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
핵심 원칙:
- 가장 큰 가변 길이 컬럼부터 차례로 처리
- 먼저 압축을 시도, 부족하면 TOAST 테이블에 외부 저장
- 외부 저장된 컬럼은 메인 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_16395TOAST 테이블의 컬럼 구조:
| 컬럼 | 의미 |
|---|---|
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 계열. 안정적이지만 압축률·속도 평범 |
| LZ4 | PG 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으로 재구성하면 새 알고리즘으로 재압축됩니다.
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를 자주 UPDATE | UPDATE는 새 tuple 작성 + 새 TOAST 청크 작성 → I/O 폭증 | 자주 바뀌는 필드는 별 컬럼으로 분리 |
| 모든 메타데이터를 JSONB 한 컬럼에 넣기 | 인덱싱·통계가 부정확, TOAST overhead 큼 | 핵심 키는 별 컬럼, 나머지만 JSONB |
| 큰 BLOB을 DB에 넣기 | TOAST는 효율적이지만 백업 크기·복제 지연을 키움 | 외부 객체 저장소 + 참조 URL/키만 DB에 |
EXTERNAL 모드 디폴트로 전환 | 압축 효과 상실, 디스크 사용량 증가 | substring 부하가 명백한 컬럼에만 한정 |
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 — 을 봅니다.