본문으로 건너뛰기
8.3 REINDEX와 인덱스 BLOAT

8.3 REINDEX와 인덱스 BLOAT

6.7에서 인덱스 BLOAT의 원인·측정·CONCURRENTLY 옵션을 봤습니다. 본 절에서는 운영 관점에서 REINDEX와 pg_repack을 언제, 어떤 단위로 어떻게 돌릴지의 절차를 정리합니다.

REINDEX 단위

REINDEX INDEX idx_orders_user_id;            -- 단일 인덱스
REINDEX TABLE orders;                         -- 한 테이블의 모든 인덱스
REINDEX SCHEMA public;                        -- 스키마 전체
REINDEX DATABASE app_main;                    -- DB 전체 (위험)
REINDEX SYSTEM postgres;                      -- 시스템 카탈로그

CONCURRENTLYINDEX/TABLE에 가능 (PG 12+). DATABASE·SCHEMA에도 CONCURRENTLY 가능하지만 매우 오래 걸립니다.

운영 중 REINDEX의 안전한 형태

-- 단일 인덱스 (가장 권장)
REINDEX INDEX CONCURRENTLY idx_orders_user_id;

-- 한 테이블의 모든 인덱스 (PG 14+에서 동작)
REINDEX TABLE CONCURRENTLY orders;
CONCURRENTLY
REINDEX INDEXPG 12+SHARE UPDATE EXCLUSIVE
REINDEX TABLEPG 14+같음
REINDEX DATABASE/SCHEMAPG 14+같음 (하지만 오래)

CONCURRENTLY 실패 후 정리

CONCURRENTLY 빌드가 deadlock·취소·crash로 실패하면 임시 인덱스가 INVALID 상태로 남습니다.

SELECT indexrelid::regclass, indisvalid
  FROM pg_index
 WHERE NOT indisvalid;

정리:

DROP INDEX CONCURRENTLY orders_user_id_ccnew;   -- _ccnew 접미사
-- 또는
REINDEX INDEX CONCURRENTLY idx_orders_user_id;  -- 다시 시도

디스크 여유 점검

CONCURRENTLY는 옛 인덱스 + 새 인덱스가 동시에 존재 → 인덱스 크기의 2배 + α 여유가 필요합니다. 큰 인덱스 REINDEX 전 디스크 잔여 확인합니다.

SELECT pg_size_pretty(pg_relation_size('idx_orders_user_id')) AS index_size;

pg_repack — 인덱스 + 테이블 둘 다

REINDEX는 인덱스만 정리합니다. 테이블 자체의 BLOAT(특히 dead tuple의 물리 공간)는 손대지 않습니다. VACUUM FULL이 답이지만 ACCESS EXCLUSIVE 락 — 운영 중 금지합니다.

pg_repack은 운영 중 무중단으로 테이블+인덱스 모두 재구성합니다.

# 한 테이블
pg_repack -d app_main -t orders

# 인덱스만
pg_repack -d app_main -t orders --only-indexes

# 스키마 전체
pg_repack -d app_main -s public

# dry-run (실제 실행 안 함)
pg_repack -d app_main -t orders --dry-run
장점단점
운영 중 무중단외부 도구, 별도 설치 필요
테이블 + 인덱스 모두임시 디스크 2배 필요
VACUUM FULL의 효과짧은 ACCESS EXCLUSIVE 락 한 번 (rename 시점)

설치:

# RHEL/Rocky
sudo dnf install -y pg_repack_17

# Debian/Ubuntu
sudo apt-get install -y postgresql-17-repack

확장 활성:

CREATE EXTENSION pg_repack;

정기 운영 절차

월간 점검

-- BLOAT 큰 인덱스
SELECT i.relname AS index,
       pg_size_pretty(pg_relation_size(i.oid)) AS size,
       round(s.avg_leaf_density, 1) AS density
  FROM pg_index x
  JOIN pg_class i ON x.indexrelid = i.oid
  JOIN pgstatindex(i.oid) s ON true
 WHERE i.relkind = 'i' AND pg_relation_size(i.oid) > 100*1024*1024
 ORDER BY s.avg_leaf_density;

density < 70이면 REINDEX 후보입니다.

분기 정비

#!/bin/bash
# 큰 인덱스 자동 REINDEX
psql -d app_main -t -c "
SELECT format('REINDEX INDEX CONCURRENTLY %I.%I;',
       schemaname, indexname)
  FROM (
    SELECT n.nspname AS schemaname, i.relname AS indexname,
           pgstatindex(i.oid).avg_leaf_density AS density
      FROM pg_class i
      JOIN pg_namespace n ON i.relnamespace = n.oid
     WHERE i.relkind = 'i' AND pg_relation_size(i.oid) > 100*1024*1024
  ) t
 WHERE density < 70;
" | psql -d app_main

야간 시간대 cron으로 실행합니다.

반기·연간

대규모 BLOAT나 테이블 자체의 dead 공간이 큰 경우 pg_repack:

pg_repack -d app_main -t orders --no-superuser-check

VACUUM FULL을 안 쓰는 이유

VACUUM FULL은 읽기·쓰기·DDL 모두 차단하는 ACCESS EXCLUSIVE 락을 잡고 테이블을 완전히 재구성합니다.

시나리오영향
100GB 테이블수 시간 이상 트래픽 정지
운영 DB절대 금지
단순 검증 환경OK
점검 시간 명확가능, 하지만 pg_repack이 더 안전

운영에서는 pg_repack 또는 분기 REINDEX CONCURRENTLY가 답입니다.

외래 키와 REINDEX

REINDEX는 인덱스만 재구성합니다. 부모-자식 관계, FK constraint는 그대로. unique constraint를 drop and recreate가 필요하면 CREATE INDEX CONCURRENTLY + ADD CONSTRAINT USING INDEX 같은 패턴입니다.

-- 새 인덱스 빌드
CREATE UNIQUE INDEX CONCURRENTLY users_email_new ON users(email);

-- 기존 인덱스 drop
ALTER TABLE users DROP CONSTRAINT users_email_key;
DROP INDEX users_email_key;

-- 새 인덱스를 unique constraint로 부착
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE USING INDEX users_email_new;

운영 안티패턴

안티패턴문제
REINDEX DATABASE 운영 중 실행모든 인덱스 ACCESS EXCLUSIVE — 클러스터 정지
VACUUM FULL을 정기 작업으로운영 시간 정지, pg_repack 권장
CONCURRENTLY 빌드 중 deadlock 무시INVALID 인덱스 누적
디스크 여유 안 보고 REINDEX진행 중 디스크 풀 → 클러스터 정지
큰 인덱스 REINDEX CONCURRENTLY 전 반드시 디스크 여유 확인. 100GB 인덱스를 재구성하면 일시적으로 200GB+가 점유됩니다. 디스크 90%+ 사용 중인 클러스터에서 시작하면 사고입니다.

정리

  • REINDEX는 인덱스만 — 테이블 자체 BLOAT는 손대지 않음
  • 운영 중에는 항상 REINDEX INDEX CONCURRENTLY 또는 REINDEX TABLE CONCURRENTLY
  • CONCURRENTLY 실패 시 INVALID 인덱스 정리 필요
  • pg_repack = 테이블 + 인덱스 모두 무중단 재구성, 운영 표준
  • VACUUM FULL은 운영 중 금지
  • 분기·반기 정비를 자동화하는 게 BLOAT 누적 방지

다음 절(8.4)에서는 운영자의 정기 점검 체크리스트를 봅니다.