6장은 분량이 짧고 성격도 단순하다. 디스크에 저장되는 데이터 파일 크기를 줄이는 이야기다. 파일이 크면 쿼리 처리(더 많은 페이지를 버퍼 풀로 읽어야 함)도, 백업·복구 시간도 다 나빠지니까. MySQL엔 압축 방법이 페이지 압축테이블 압축 두 가지가 있는데, 결론부터 말하면 하나는 실무에서 거의 안 쓰고 하나만 쓸 만하다.

# 6.1 페이지 압축 — 좋아 보이지만 실무에선 안 쓴다

페이지 압축(Transparent Page Compression)은 디스크에 쓸 때만 압축하고, 버퍼 풀로 읽어올 때 해제한다. 버퍼 풀에는 항상 압축이 풀린 상태로 있으니 InnoDB 코드 입장에선 "투명(Transparent)"하다 — 그래서 이름이 그렇다.

문제는 구현 방식이다. 16KB 페이지를 압축하면 결과 크기가 얼마가 될지 미리 알 수 없는데, 디스크의 페이지 크기는 정해져 있다. 그래서 **펀치 홀(Punch Hole)**이라는 파일 시스템 기능을 쓴다.

  1. 16KB 페이지를 압축 (결과 7KB 가정)
  2. 디스크에 압축된 7KB를 기록 (뒤 9KB는 빈 데이터로 남음)
  3. 그 뒤 빈 9KB 구간에 펀치 홀을 뚫어 파일 시스템에 반납
  4. 그럼 디스크는 실제로 7KB만 차지

읽을 때는 압축된 7KB와 펀치 홀 9KB를 합쳐 16KB로 읽어들인다.

여기에 치명적인 한계가 있다. 펀치 홀은 운영체제·파일 시스템뿐 아니라 하드웨어까지 지원해야 하고, 지원 환경이 제한적이다. 게다가 결정적으로 — cp 같은 일반 복사 명령이나 Xtrabackup으로 파일을 복사하면 펀치 홀이 다시 채워져서, 원본 데이터 파일이 1GB였어도 복사본은 10GB로 부풀 수 있다. 백업·복구 과정에서 이러면 압축한 의미가 사라진다.

이런 이유로 실무에서는 페이지 압축을 거의 안 쓴다. 책도 "자세한 설명은 생략한다"며 빠르게 넘어간다. 우리도 "이런 게 있다"만 알고 넘어가면 된다.

# 6.2 테이블 압축 — 이걸 쓴다

테이블 압축은 운영체제·하드웨어 제약 없이 쓸 수 있어서 활용도가 높다. 다만 공짜는 아니고 단점 세 가지가 있다.

  • 버퍼 풀 공간 활용률이 낮아진다 — 압축본과 압축 해제본을 둘 다 들고 있어야 해서
  • 쿼리 처리 성능이 낮아진다 — 압축 해제에 CPU를 쓰니까
  • 변경이 잦으면 압축 실패율이 올라간다 — 뒤에서 설명

사용법은 테이블 생성 시 ROW_FORMAT=COMPRESSED를 명시하고, 압축될 페이지의 목표 크기KEY_BLOCK_SIZE로 지정한다(4KB·8KB 등, 2의 제곱). 전제로 innodb_file_per_table=ON이어야 한다.

SET GLOBAL innodb_file_per_table=ON;
CREATE TABLE compressed_table (
  c1 INT PRIMARY KEY
) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;

# 압축 실패(compression failure)가 핵심이다

KEY_BLOCK_SIZE를 잘못 잡으면 성능이 급락하는데, 그 메커니즘이 이렇다. 16KB 원본 페이지를 압축했는데 결과가 KEY_BLOCK_SIZE(예: 8KB)를 초과하면, InnoDB는 그 페이지를 둘로 쪼개서(split) 각각 다시 압축을 시도한다. 이 "초과해서 다시 하는" 게 압축 실패다. 실패가 잦으면 그만큼 CPU를 태우고, 특히 버퍼 풀에서 디스크로 페이지가 내려갈 때마다 재압축이 걸려 성능이 크게 떨어진다.

KEY_BLOCK_SIZE는 찍지 말고 실측하라

목표 크기를 감으로 정하면 안 된다. innodb_cmp_per_index_enabled=ON으로 켜고 샘플 데이터를 넣은 뒤, information_schema.INNODB_CMP_PER_INDEX에서 인덱스별 압축 성공/실패 횟수를 직접 보고 결정해야 한다. 샘플은 최소 테이블 데이터 페이지가 10개 정도는 생기도록 넣는 게 좋다.

SET GLOBAL innodb_cmp_per_index_enabled=ON;
INSERT INTO employees_comp4k SELECT * FROM employees;

SELECT table_name, index_name, compress_ops, compress_ops_ok,
       (compress_ops-compress_ops_ok)/compress_ops * 100 as compression_failure_pct
  FROM information_schema.INNODB_CMP_PER_INDEX;

책의 실측 결과가 교훈적이다. 같은 employees 데이터를 KEY_BLOCK_SIZE=4=8로 각각 압축해봤더니:

PRIMARY 키 압축 실패율 디스크 파일 크기
원본 (압축 없음) 30MB
KEY_BLOCK_SIZE=4 27.67% 20MB
KEY_BLOCK_SIZE=8 18.52% 21MB (거의 동일)

흥미로운 결론 — 4KB(20MB)와 8KB(21MB)의 최종 파일 크기가 거의 같다. 압축률이 비슷하다면, 굳이 실패율 27.67%짜리 4KB를 쓸 이유가 없다. 압축 실패율이 낮은 8KB를 고르는 게 훨씬 효율적이다. 압축으로 인한 용량 이득은 그대로 챙기면서 재압축 CPU 낭비만 줄이는 것. 일반적으로 압축 실패율이 3~5% 미만으로 유지되게 KEY_BLOCK_SIZE를 잡는 게 좋다.

# 압축 페이지의 버퍼 풀 적재

압축된 페이지가 버퍼 풀에서 어떻게 관리되냐면 — InnoDB는 압축본과 압축 해제본 2개 버전을 동시에 들고 있다. 압축 해제본은 별도의 Unzip_LRU 리스트로 관리한다. 그리고 서버의 부하 패턴에 따라 **적응적(Adaptive)**으로 조절한다.

  • CPU 사용률이 높은 서버 → 압축 해제본을 더 많이 유지 (재압축·재해제를 피하려고)
  • 디스크 I/O 사용률이 높은 서버 → 압축 해제본을 버려 버퍼 풀 공간을 더 확보

즉 테이블 압축은 버퍼 풀 공간을 압축본·해제본으로 이중으로 쓰기 때문에 메모리를 낭비하는 면이 있는데, 이 두 단점(메모리 낭비 vs 재압축 CPU 비용)을 상황에 맞춰 저울질하는 게 Unzip_LRU의 역할이다.

🐘 PostgreSQL 비교 — PG에는 이런 "테이블 통째 압축(KEY_BLOCK_SIZE)" 옵션이 없다. 대신 큰 값(대략 2KB 초과)을 자동으로 압축·분리 저장하는 **TOAST(The Oversized-Attribute Storage Technique)**가 컬럼 단위로 늘 돌아간다. 페이지 전체를 목표 크기에 맞춰 재압축하고 실패율을 튜닝하는 InnoDB식 발상 자체가 PG엔 없다.

# 정리

6장에서 실무로 들고 갈 것.

  • 페이지 압축은 안 쓴다. 펀치 홀이 하드웨어·파일 시스템 지원을 타고, 복사하면 다시 부풀어서 실무에서 거의 안 쓴다. "이런 게 있다"만 알면 된다.
  • 테이블 압축을 쓸 거면 KEY_BLOCK_SIZE를 찍지 말고 실측하라. INNODB_CMP_PER_INDEX로 압축 실패율을 보고 3~5% 미만으로 유지되는 값을 골라라.
  • 파일 크기가 비슷하면 실패율 낮은(더 큰) KEY_BLOCK_SIZE가 낫다. 책 예시에서 4KB·8KB 최종 크기가 거의 같았고, 그럼 실패율 낮은 8KB가 정답이다.
  • 변경이 잦은 테이블은 압축을 재고하라. 데이터가 자주 바뀌면 재압축이 계속 걸려 CPU를 태운다. 압축은 로그성·이력성처럼 한 번 쓰고 잘 안 바뀌는 데이터에 잘 맞는다.