본문 바로가기
데이터베이스/PostgreSQL 내부 구조

PostgreSQL 내부구조 (8) ㅡ TOAST

by 조민서 2026. 9. 25.

PostgreSQL 내부 구조 시리즈

시작. 시리즈를 시작하며

0. 연결과 프로세스 구조

1. 8KB 페이지와 Shared Buffers

2. MVCC와 격리 수준

3. Locking

4. VACUUM

5. WAL과 Checkpoint

6. Index

7. Scan과 Optimizer

8. TOAST

정리. 1~8편 총정리

 

1편에서는 PostgreSQL의 Heap이 기본적으로 8KB Page 단위로 저장된다고 살펴봤다.

Page 안에는 여러 Tuple이 들어가지만, 하나의 Heap Tuple은 Page 경계를 넘어 저장될 수 없다.

 

그런데 PostgreSQL에서는 TEXT, JSONB, BYTEA 같은 컬럼에 하나의 Page보다 훨씬 큰 값을 저장할 수 있다.

CREATE TABLE documents (
    id bigint,
    content text,
    metadata jsonb
);

 

PostgreSQL은 이런 큰 값을 Tuple 안에 그대로 넣으려고 하지 않는다.

필요에 따라 값을 압축하고, 그래도 Row가 크다면 실제 값을 Main Heap 밖으로 분리한다.

이 구조가 TOAST(The Oversized-Attribute Storage Technique)다.

 

목차

  1. 8KB Page와 TOAST
    - Heap Tuple은 Page를 넘어 저장되지 않는다

    - varlena와 TOAST 대상 컬럼
    - TOAST가 시작되는 기준
  2. 큰 값은 어떻게 저장되는가
    - 압축과 Out-of-Line Storage

    - TOAST Pointer
    - Chunk와 TOAST Table
  3. TOAST 데이터를 읽고 수정하는 과정
    - Detoast

    - 필요한 컬럼만 읽는 이유
    - UPDATE와 TOAST
  4. Storage Strategy와 압축
    - PLAIN / EXTENDED / EXTERNAL / MAIN

    - pglz와 lz4
  5. TOAST를 사용할 때 알아둘 점
    - 저장 상태 확인

    - 최대 크기
    - 큰 컬럼의 읽기·쓰기 패턴

1. 8KB Page와 TOAST

PostgreSQL의 Heap은 기본적으로 8KB Page 단위로 관리된다.

Page에는 Page Header와 Line Pointer, 실제 Heap Tuple들이 함께 들어간다.

8KB Heap Page

 

여기서 중요한 제약은 하나의 Heap Tuple이 여러 Heap Page에 걸쳐 저장되지 않는다는 것이다.

예를 들어 Tuple의 앞부분을 Page 10에 저장하고 나머지를 Page 11에 이어서 저장하는 방식으로 처리하지 않는다.

 

따라서 Tuple이 너무 커지면 Row 자체의 크기를 줄일 방법이 필요하다.

이 문제는 특히 다음과 같은 가변 길이 타입에서 발생한다.

  • text
  • jsonb
  • bytea
  • 가변 길이 Array

이러한 타입은 내부적으로 varlena(variable-length) 형식을 사용하는 경우가 많다.

영역 역할
Header 값의 길이와 저장 형태를 해석하는 데 필요한 정보
Data 실제 값 또는 외부 저장된 값을 가리키는 정보

varlena 값은 Inline으로 저장될 수도 있고, 압축되거나 외부 TOAST Table에 저장된 형태로 표현될 수도 있다.

 

TOAST는 8KB를 넘은 뒤에만 동작하는 것이 아니다

TOAST는 Row가 정확히 8KB를 넘는 순간에 처음 동작하는 구조가 아니다.

기본 설정에서는 Row가 대략 2KB 수준보다 커지면 TOAST 가능한 Attribute를 대상으로 Row 크기를 줄이려고 시도한다.

Row가 커짐 → TOAST 가능한 Attribute 확인 → 압축 또는 외부 저장 → Main Heap의 Tuple 크기 축소

정확한 기준은 PostgreSQL의 Page 크기와 내부 설정에 영향을 받기 때문에, 2KB를 절대적인 경계라기보다 기본적인 TOAST Target에 가까운 값으로 이해하는 것이 좋다.

  • 이렇게 큰 Attribute를 분리하면 하나의 큰 Row가 Page 대부분을 차지하는 것을 줄일 수 있다.
  • Main Heap의 Tuple이 작아지면 같은 수의 Heap Page와 Shared Buffers 안에 더 많은 Tuple을 유지할 수도 있다.

2. 큰 값은 어떻게 저장되는가

대부분의 TOAST 가능한 데이터 타입은 기본적으로 EXTENDED Storage Strategy를 사용한다.

EXTENDED에서는 큰 Attribute가 있으면 먼저 압축을 시도하고, Row가 여전히 크다면 값을 Main Heap 밖으로 이동할 수 있다.

PostgreSQL TOAST 저장 구조

 

압축과 Out-of-Line Storage

먼저 압축을 통해 값 자체의 크기를 줄일 수 있는지 확인한다.

예를 들어 20KB의 값이 압축을 통해 훨씬 작아질 수 있다면 압축된 형태로 저장할 수 있다.

 

하지만 압축만으로 Tuple을 충분히 작게 만들 수 없다면 실제 Attribute 값을 Main Heap 밖의 TOAST Table로 이동한다.

이를 Out-of-Line Storage라고 한다.

 

Main Tuple에는 실제 큰 값 대신 작은 TOAST Pointer가 남는다.

Main Tuple → TOAST Pointer → TOAST Table의 실제 값

 

큰 값은 Chunk로 나뉘어 저장된다

TOAST Table 역시 PostgreSQL의 Heap Table이기 때문에 수 MB 크기의 값을 하나의 거대한 Tuple로 저장하는 것은 아니다.

외부로 이동한 값은 약 2KB 크기의 여러 Chunk로 나뉘어 저장된다.

 

TOAST Table의 핵심적인 구조를 단순화하면 다음과 같다.

Chunk_id chunk_seq chunk_data
100 0 ...
100 1 ...
100 2 ...
100 3 ...
100 4 ...

chunk_id가 같은 Row들은 하나의 원래 값을 구성하고, chunk_seq는 Chunk의 순서를 나타낸다.

PostgreSQL은 이 Chunk들을 다시 조합해서 원래 값을 복원한다.

 

TOAST Pointer는 외부 값의 위치를 나타낸다

Main Tuple에 남아 있는 TOAST Pointer에는 외부 값을 찾고 복원하는 데 필요한 정보가 들어 있다.

정보 의미
TOAST Table 실제 외부 값이 저장된 TOAST Relation
chunk_id 외부에 저장된 값을 식별하는 ID
원래 크기 압축 전 원본 값의 크기
저장된 크기 실제 저장된 값의 크기
압축 방식 값이 압축된 경우 이를 해석하는 데 필요한 정보

따라서 수 MB 크기의 JSONB가 들어 있어도 Main Heap의 Tuple에서는 실제 전체 데이터 대신 작은 Pointer 형태로 유지될 수 있다.

 

Table과 연결된 TOAST Table은 다음과 같이 확인할 수 있다.

SELECT
    relname,
    reltoastrelid::regclass
FROM pg_class
WHERE oid = 'documents'::regclass;

TOAST Table이 존재한다면 일반적으로 pg_toast.pg_toast_... 형태의 내부 Relation이 연결되어 있다.


3. TOAST 데이터를 읽고 수정하는 과정

TOAST가 사용된다고 해서 모든 Query가 항상 TOAST Table을 읽는 것은 아니다.

 

다음 Table을 예로 들어보자.

CREATE TABLE documents (
    id bigint,
    title text,
    content text
);

여기서 content가 매우 크고 TOAST Table에 Out-of-Line으로 저장되어 있다고 하자.

 

TOAST 컬럼을 읽을 때와 읽지 않을 때

필요한 컬럼만 읽으면 TOAST 접근을 피할 수 있다

다음 Query는 큰 content 값을 결과로 요구하지 않는다.

SELECT id, title
FROM documents;

이 경우 Query 처리에 content가 필요하지 않다면 외부 TOAST Value를 가져오지 않고 Main Tuple의 다른 Column만 처리할 수 있다.

 

반면 다음 Query는 모든 Column을 요구한다.

SELECT *
FROM documents;

content가 Out-of-Line으로 저장되어 있다면 해당 값을 반환하기 위해 TOAST Table에서 Chunk를 가져와 원래 값을 재구성해야 한다.

 

필요한 경우 압축된 값의 압축도 해제한다.

이처럼 외부 저장되거나 압축된 값을 실제 사용할 수 있는 형태로 가져오는 과정을 일반적으로 Detoast라고 부른다.

 

큰 TOAST 컬럼을 읽을 때는 다음과 같은 추가 작업이 발생할 수 있다.

  • Main Heap 접근
  • TOAST Index 접근
  • TOAST Heap 접근
  • Chunk 재구성
  • 필요한 경우 압축 해제
  • Client로 큰 결과를 보내기 위한 Network 전송

따라서 TOAST는 큰 값을 읽는 비용을 없애는 기능이라기보다 큰 값의 비용을 실제로 필요한 순간까지 Main Heap 접근에서 분리하는 구조라고 보는 편이 정확하다.

 

UPDATE와 TOAST

PostgreSQL의 UPDATE는 2편에서 살펴본 MVCC 구조와 함께 동작한다.

기존 Tuple을 같은 위치에서 직접 덮어쓰는 대신 새로운 Tuple Version을 만든다.

 

하지만 Row에 큰 TOAST Column이 있다고 해서 작은 Column을 수정할 때마다 큰 값을 반드시 새로 TOAST 처리하는 것은 아니다.

UPDATE documents
SET title = 'new title'
WHERE id = 1;

content 자체가 변경되지 않았다면 새 Tuple Version이 기존 Out-of-Line TOAST Value를 그대로 참조할 수 있다.

 

반대로 큰 Attribute 자체를 변경하면 새로운 값에 대해 다시 TOAST 처리가 필요하다.

UPDATE documents
SET content = ...
WHERE id = 1;
새로운 Large Value → 압축 시도 → Out-of-Line 여부 판단 → 필요한 경우 Chunk 저장

따라서 큰 TEXT나 JSONB 값을 자주 통째로 변경하는 구조에서는 작은 Row를 수정하는 경우보다 더 많은 저장 작업과 WAL 기록이 발생할 수 있다.


4. Storage Strategy와 압축

PostgreSQL은 TOAST 가능한 Column마다 네 가지 Storage Strategy를 제공한다.

Strategy 압축 Out-of-Line 특징
PLAIN X X 값을 Main Heap에 Inline으로 저장
EXTENDED O O 압축을 시도하고, 필요하면 TOAST Table로 외부 저장
EXTERNAL X O 압축하지 않고 외부 저장 가능
MAIN O 가능하면 피함 압축은 허용하지만 가능한 한 Main Heap에 유지

 

PLAIN

PLAIN은 값을 Main Heap의 Tuple 안에 Inline으로 저장한다.

압축이나 Out-of-Line Storage를 사용하지 않으며, TOAST가 적용되지 않는 고정 길이 타입 등에서 사용된다.

 

EXTENDED

EXTENDED는 대부분의 TOAST 가능한 데이터 타입이 기본적으로 사용하는 방식이다.

큰 값에 대해 압축을 시도하고, Row를 충분히 줄일 수 없다면 Out-of-Line Storage도 허용한다.

큰 값 → 압축 → 그래도 Row가 크면 TOAST Table로 이동

 

EXTERNAL

EXTERNAL은 Out-of-Line Storage는 허용하지만 값 자체를 압축하지 않는다.

압축된 전체 값을 복원하는 비용을 피해야 하는 특정 접근 패턴에서는 의미가 있을 수 있다.

 

MAIN

MAIN은 값을 압축할 수 있지만 가능한 한 Main Heap 안에 유지하려고 한다.

다만 Tuple을 Page 안에 저장할 수 있도록 만들기 위해 필요한 경우에는 최종적으로 Out-of-Line Storage가 발생할 수 있다.

 

Storage Strategy는 다음처럼 변경할 수 있다.

ALTER TABLE documents
ALTER COLUMN content
SET STORAGE EXTERNAL;

이 설정을 변경했다고 기존에 저장된 모든 Row가 즉시 새로운 방식으로 다시 작성되는 것은 아니다.

이후 새롭게 저장되거나 해당 값이 다시 작성될 때 새로운 설정이 적용될 수 있다.

 

TOAST 압축 방식

TOAST 가능한 값에는 PostgreSQL의 pglz 압축을 사용할 수 있고, LZ4 지원이 포함된 환경에서는 lz4도 사용할 수 있다.

ALTER TABLE documents
ALTER COLUMN content
SET COMPRESSION lz4;

 

Table을 생성할 때 지정할 수도 있다.

CREATE TABLE documents (
    id bigint,
    content text COMPRESSION lz4
);

 

압축은 저장 공간과 I/O를 줄일 수 있지만 압축과 해제 과정에서는 CPU를 사용한다.

따라서 특정 압축 방식이 항상 더 좋다기보다 데이터가 얼마나 잘 압축되는지와 읽기·쓰기 패턴을 함께 봐야 한다.


5. TOAST를 사용할 때 알아둘 점

실제 저장 상태 확인하기

Column 값이 실제로 얼마나 많은 공간을 차지하는지는 다음 함수로 확인할 수 있다.

SELECT pg_column_size(content)
FROM documents;

 

압축된 값의 압축 방식은 다음처럼 확인할 수 있다.

SELECT pg_column_compression(content)
FROM documents;

 

외부 TOAST Value의 Chunk ID를 확인할 수 있는 PostgreSQL 버전에서는 다음과 같은 함수도 사용할 수 있다.

SELECT pg_column_toast_chunk_id(content)
FROM documents;

 

TOAST에도 크기 제한은 있다

TOAST를 사용한다고 해서 하나의 Column에 무한한 크기의 데이터를 저장할 수 있는 것은 아니다.

일반적인 TOAST 가능한 varlena 값의 최대 크기는 약 1GB 수준이다.

text나 bytea 하나에 수 GB의 데이터를 그대로 넣는 것은 TOAST가 해결하려는 범위를 넘어선다.

 

큰 값을 저장할 수 있다는 것과 그렇게 설계하는 것은 별개의 문제다

예를 들어 다음과 같은 Table이 있다고 하자.

Column 크기 예시
id 8B
created_at 8B
name 50B
payload 500KB

 

대부분의 Query가 다음처럼 작은 Column만 사용한다면 큰 payload를 Main Heap 밖으로 분리할 수 있다는 점은 유리하다.

SELECT id, created_at
FROM events;

 

Main Heap을 읽는 일반적인 Query에서 500KB의 값을 매번 가져오지 않아도 되기 때문이다.

 

반대로 거의 모든 요청에서 payload 전체를 읽고 자주 통째로 수정한다면 Detoast, TOAST Table 접근, 새로운 Chunk 저장 같은 비용이 반복될 수 있다.

따라서 큰 JSONB나 TEXT 값을 저장할 수 있다는 사실만 보고 Table 구조를 결정하기보다 실제로 어떤 Column을 얼마나 자주 읽고 수정하는지를 함께 봐야 한다.

 

TOAST의 전체 흐름

저장할 때의 흐름을 정리하면 다음과 같다.

INSERT / UPDATE → Tuple 생성 → Row가 커짐 → TOAST 대상 Attribute 확인 → 압축 → 필요하면 Chunk로 분할 → TOAST Table 저장 → Main Tuple에는 Pointer 저장

 

읽을 때는 필요한 Attribute인지에 따라 달라진다.

Main Heap 조회 → 큰 Attribute가 필요하지 않음 → TOAST Table 접근 없이 처리

 

반대로 큰 Attribute가 필요하면 다음 과정이 추가된다.

Main Heap → TOAST Pointer → TOAST Chunk 조회 → 값 재구성 → 필요한 경우 압축 해제

 

결국 TOAST의 목적은 큰 데이터의 비용 자체를 없애는 것이 아니다.

8KB Page라는 기존 Heap 구조를 유지하면서 큰 Attribute를 일반적인 Row 접근에서 분리하고, 필요할 때만 가져올 수 있게 만드는 것이다.

 

1편에서 시작한 8KB Page라는 제약은 8편까지 그대로 유지된다.

PostgreSQL은 Page 크기의 제한을 없애는 대신, 큰 값만 압축하거나 Page 밖으로 분리하는 방식으로 큰 데이터를 저장한다.

이것으로 PostgreSQL 내부구조 시리즈의 개별 주제를 마무리한다.