감사 로그의 이벤트 테이블을 위한 PostgreSQL 파티셔닝
PostgreSQL 파티셔닝을 통해 이벤트 테이블에서 언제 이득이 나는지, 파티션 키는 어떻게 고르는지, 관리자 패널 필터와 보존(리텐션)에 어떤 영향이 있는지 알아보세요.

이벤트와 감사 테이블이 문제되는 이유
이벤트 테이블과 감사 테이블은 비슷해 보이지만 목적이 다릅니다.
이벤트 테이블은 페이지 조회, 전송된 이메일, 웹훅 호출, 작업 실행처럼 일어난 일을 기록합니다. 감사 테이블은 누가 언제 무엇을 바꿨는지 기록합니다: 상태 변경, 권한 업데이트, 지급 승인 같은 항목으로 종종 변경 전/후 내용을 포함합니다.
두 테이블 모두 추가만 일어나는(append-only) 특성 때문에 빠르게 커집니다. 개별 행을 자주 삭제하지 않고, 매분 새 행이 들어옵니다. 배경 작업과 통합을 포함하면 작은 제품도 몇 주 만에 수백만 행의 로그를 생성할 수 있습니다.
문제는 일상 업무에서 드러납니다. 관리자 패널은 보통 "어제의 오류"나 "이 사용자의 활동" 같은 빠른 필터를 필요로 합니다. 테이블이 커질수록 그런 기본 화면들이 느려지기 시작합니다.
초기에 보통 몇 가지 증상이 나타납니다:
- 좁은 날짜 범위임에도 필터가 몇 초 걸리거나(또는 시간 초과) 실패한다.
- 인덱스가 너무 커져 삽입이 느려지고 저장 비용이 증가한다.
- VACUUM 및 autovacuum이 오래 걸리고 유지보수가 눈에 띄게 된다.
- 보존(데이터 삭제)이 위험해진다: 오래된 행 삭제가 느리고 블로트가 생긴다.
파티셔닝은 이런 문제를 다루는 한 방법입니다. 간단히 말하면, 하나의 큰 테이블을 여러 작은 테이블(파티션)로 나누고 논리적으로 같은 이름을 공유하게 합니다. PostgreSQL은 보통 시간에 기반한 규칙으로 새 행을 적절한 파티션으로 라우팅합니다.
그래서 팀들은 이벤트 테이블에 PostgreSQL 파티셔닝을 검토합니다: 최근 데이터는 작게 유지되어, 쿼리가 특정 시간 창만 필요할 때 PostgreSQL이 전체 파티션을 건너뛸 수 있기 때문입니다.
파티셔닝이 만능은 아닙니다. "최근 일주일" 같은 쿼리에 큰 도움이 되고 보존을 단순화(오래된 파티션 드롭)하지만, 새로운 문제를 만들 수도 있습니다:
- 파티션 키를 사용하지 않는 쿼리는 많은 파티션을 확인해야 할 수 있다.
- 파티션 수가 늘어나면 관리해야 할 객체가 많아지고 잘못 구성될 가능성이 커진다.
- 일부 고유 제약과 인덱스는 전체 데이터에 걸쳐 적용하기 어려워진다.
관리자 패널이 날짜 필터와 예측 가능한 보존 규칙에 크게 의존한다면, 파티셔닝은 실질적인 이득이 될 수 있습니다. 반면에 대부분의 쿼리가 "사용자 X의 전체 활동"이라면, UI와 인덱스를 신중히 설계하지 않으면 오히려 골치거리가 될 수 있습니다.
로그와 감사의 전형적인 접근 패턴
이벤트와 감사 테이블은 일방향으로 성장합니다: 위로 계속 쌓입니다. 지속적인 삽입이 있고 거의 업데이트가 없습니다. 대부분의 행은 한 번 쓰이고 나중에 지원, 사고 조사 또는 규정 준수 확인 시에 읽힙니다.
이 "append-only" 형태가 중요합니다. 삽입 성능은 하루 종일 관심사이고, 읽기 성능은 지원이나 운영이 빠른 답을 필요로 할 때 집중적으로 중요해집니다.
대부분의 읽기는 필터입니다. 관리자 패널에서는 보통 사용자가 넓게 시작(최근 24시간)한 뒤 사용자, 엔티티, 액션으로 좁혀갑니다.
일반적인 필터에는 다음이 포함됩니다:
- 시간 범위
- 행위자(사용자 ID, 서비스 계정, IP 주소)
- 대상(엔티티 타입 + 엔티티 ID, 예: 주문 #1234)
- 액션 타입(생성, 수정, 삭제, 로그인 실패)
- 상태나 심각도(성공/오류)
시간 범위는 자연스러운 "첫 번째 자르기"입니다. 이것이 이벤트 테이블 파티셔닝의 핵심 통찰입니다: 많은 쿼리는 시간 조각을 원하고, 그 안에서 다른 필터들이 적용됩니다.
보존도 또 다른 상수입니다. 로그는 거의 영구적으로 보관하지 않습니다. 팀은 보통 고세부 이벤트를 30일 또는 90일 보관한 뒤 삭제하거나 아카이브합니다. 감사 로그는 더 긴 요구사항(365일 이상)이 있을 수 있지만, 데이터베이스를 블로킹하지 않고 오래된 데이터를 제거할 예측 가능한 방법을 원합니다.
감사 로깅은 추가 기대사항을 동반합니다. 기록은 보통 불변(immutable)을 원하고, 모든 레코드가 추적 가능(누가/무엇을/언제 및 요청/세션 컨텍스트 포함)해야 하며, 접근 제어가 필요합니다(모든 사람이 보안 관련 이벤트를 보지 못하게).
이 패턴들은 UI 설계에 직접 반영됩니다. 사람들이 기본적으로 기대하는 필터—날짜 선택기, 사용자 선택기, 엔티티 검색, 액션 드롭다운—가 바로 테이블과 인덱스가 지원해야 할 필터입니다. 그래야 볼륨이 증가해도 관리자 경험이 빠르게 유지됩니다.
파티셔닝이 가치가 있는지 판단하는 방법
파티셔닝이 감사 로그에 대해 기본 권장 사항은 아닙니다. 일상적인 쿼리와 루틴 유지보수가 서로 충돌하기 시작할 만큼 하나의 테이블이 커졌을 때 가치가 있습니다.
간단한 사이즈 기준: 이벤트 테이블이 수천만 행에 도달하면 측정해볼 가치가 있습니다. 테이블과 인덱스가 수십 기가바이트로 커지면, 단순한 날짜 범위 검색조차 느려지거나 예측 불가능해질 수 있습니다. 디스크에서 더 많은 데이터 페이지를 읽고 인덱스 유지비용이 커지기 때문입니다.
가장 명확한 쿼리 신호는 자주 작은 시간 조각(지난 하루, 지난 주)을 요청하면서도 PostgreSQL이 여전히 테이블의 많은 부분을 건드릴 때입니다. "최근 활동" 화면이 느리거나 날짜+사용자, 액션 타입, 엔티티 ID로 필터한 감사가 느릴 때입니다. 쿼리 계획에서 큰 스캔이 보이거나 버퍼 읽기가 지속적으로 높다면 불필요한 데이터를 읽고 있는 것입니다.
유지보수 신호도 중요합니다:
- VACUUM과 autovacuum이 예전보다 훨씬 오래 걸린다.
- Autovacuum이 뒤처지고 죽은 튜플(블로트)이 쌓인다.
- 특히 다중 컬럼 인덱스가 예상보다 빠르게 커진다.
- 유지보수와 트래픽이 겹칠 때 잠금 경합이 눈에 띄게 된다.
운영 비용은 팀을 파티셔닝으로 밀어넣는 느린 흐름입니다. 백업과 복원이 느려지고 저장공간이 증가하며, 큰 DELETE가 블로트와 추가 vacuum 작업을 만들어 보존 작업이 비싸집니다.
주된 목표가 명확한 보존 정책과 "최근 기간" 쿼리의 가속이라면 파티셔닝은 진지하게 고려할 가치가 있습니다. 반대로 테이블 규모가 적당하고 인덱스가 잘 되어 있어 쿼리가 이미 빠르다면, 파티셔닝은 명확한 이득 없이 복잡성만 더할 수 있습니다.
이벤트 및 감사 테이블에 적합한 파티셔닝 옵션
대부분의 감사 및 이벤트 데이터에는 시간에 의한 범위 파티셔닝(range by time)이 가장 단순하고 적합한 선택입니다. 로그는 시간 순서로 들어오고, 쿼리는 종종 "최근 24시간" 또는 "최근 30일" 같은 시간 창에 초점을 맞추며, 보존도 보통 시간 기반입니다. 시간 파티션을 사용하면 오래된 데이터를 드롭하는 것이 큰 DELETE를 실행하는 것보다 훨씬 빠릅니다.
시간 범위 파티셔닝은 인덱스도 더 작고 집중되게 유지합니다. 각 파티션은 자체 인덱스를 가지므로 지난주를 조회하는 쿼리는 수년치를 아우르는 거대한 인덱스를 훑을 필요가 없습니다.
다른 파티셔닝 방식도 있지만, 로그 및 감사 사례에는 덜 적합합니다:
- 리스트(List, 예: 테넌트별)는 소수의 매우 큰 테넌트가 있고 쿼리가 주로 한 테넌트 내에서 이루어질 때 유효할 수 있습니다. 테넌트가 수백~수천 개가 되면 관리가 고통스러워집니다.
- 해시(Hash)는 시간 기반 쿼리가 없고 쓰기를 고르게 분산하고 싶을 때 도움이 됩니다. 감사 로그에서는 보존 및 시간 기반 탐색을 어렵게 하므로 덜 일반적입니다.
- 서브파티셔닝(시간 + 테넌트)은 강력할 수 있지만 복잡도가 빠르게 증가합니다. 매우 높은 볼륨 시스템과 엄격한 테넌트 분리가 필요한 경우에 주로 사용됩니다.
시간 범위를 선택한다면, 탐색과 보존 방식에 맞는 파티션 크기를 고르세요. 매우 높은 볼륨이나 엄격한 보존이 필요하면 일간 파티션이 의미가 있고, 중간 볼륨이면 월간 파티션이 관리하기 쉽습니다.
실용적 예: 관리자팀이 매일 실패한 로그인 시도를 확인하고 지난 7일로 필터한다면, 일별 또는 주별 파티션은 쿼리가 최신 파티션만 건드리게 하여 PostgreSQL이 나머지를 무시하게 합니다.
어떤 접근을 선택하든, 미래 파티션 생성, 늦게 도착하는 이벤트 처리, 경계(일 마감, 월 마감)에서의 동작 정의 같은 지루한 부분을 계획하세요. 이러한 루틴이 단순하게 유지될 때 파티셔닝은 가치를 발휘합니다.
적절한 파티션 키 선택 방법
좋은 파티션 키는 다이어그램에서 데이터가 어떻게 보이는지가 아니라 테이블을 어떻게 읽는지와 맞아야 합니다.
이벤트 및 감사 로그의 경우 관리자 패널에서 사람들이 첫 번째로 어떤 필터를 사용하는지부터 시작하세요. 대부분의 팀은 시간 범위(최근 24시간, 최근 7일, 사용자 지정 날짜)를 사용합니다. 이것이 사실이라면 시간 기반 파티셔닝이 가장 크고 예측 가능한 이득을 줍니다.
키를 장기적인 약속으로 다루세요. 수년간 계속할 쿼리를 최적화하는 것입니다.
사람들이 먼저 쓰는 "첫 번째 필터"로 시작하세요
대부분의 관리자 화면은 시간 범위와 선택적 사용자, 액션, 상태, 리소스로 구성됩니다. 결과를 일찍 꾸준히 좁히는 항목으로 파티션하세요.
간단한 현실 검사:
- 기본 뷰가 "최근 이벤트"라면 타임스탬프를 기준으로 파티션하세요.
- 기본 뷰가 "한 테넌트/계정의 이벤트"라면
tenant_id가 의미가 있을 수 있지만, 테넌트가 충분히 커야 그 선택이 정당화됩니다. - 첫 단계가 항상 "사용자 선택"이라면
user_id로의 파티셔닝은 유혹적일 수 있으나 관리해야 할 파티션 수가 지나치게 늘어납니다.
고카디널리티 키를 피하세요
파티셔닝은 각 파티션이 의미 있는 데이터 청크일 때 가장 잘 작동합니다. user_id, session_id, request_id, device_id 같은 키는 수천~수백만의 파티션을 만들 수 있습니다. 이는 메타데이터 오버헤드를 증가시키고 유지보수를 복잡하게 하며 쿼리 플래닝을 느리게 할 수 있습니다.
시간 기반 파티션은 파티션 수를 예측 가능하게 유지합니다. 일간, 주간, 월간 중에서 볼륨에 따라 선택하세요. 너무 적으면(연간) 도움이 적고, 너무 많으면(시간별) 오버헤드가 빨리 커집니다.
올바른 타임스탬프 선택: created_at vs occurred_at
시간이 무엇을 의미하는지 명확히 하세요:
occurred_at: 이벤트가 제품에서 실제로 발생한 시간.created_at: 데이터베이스가 이를 기록한 시간.
감사의 경우 보통 "언제가 발생했는가"가 중요합니다. 하지만 배달 지연(오프라인 모바일, 재시도, 큐) 때문에 occurred_at이 늦게 들어오는 경우가 있습니다. 늦은 도착이 흔하면 created_at으로 파티셔닝하고 occurred_at을 필터용 인덱스로 두는 것이 운영상 더 안정적일 수 있습니다. 다른 옵션은 명확한 백필 정책을 정의하고 오래된 파티션이 가끔 늦은 이벤트를 받는 것을 허용하는 것입니다.
또한 시간을 저장하는 방식을 결정하세요. 일관된 타입(보통 timestamptz)을 사용하고 UTC를 소스 오브 트루스로 삼으세요. 뷰어의 타임존은 UI에서 렌더링하세요. 이렇게 하면 파티션 경계가 안정적이고 서머타임 같은 문제가 줄어듭니다.
단계별: 파티셔닝 계획 및 롤아웃
파티셔닝은 빠른 수정이 아니라 작은 마이그레이션 프로젝트로 취급할 때 가장 쉽습니다. 목표는 단순한 쓰기, 예측 가능한 읽기, 그리고 보존이 일상적인 작업이 되는 것입니다.
실용적인 롤아웃 계획
-
볼륨에 맞는 파티션 크기를 선택하세요. 월간 파티션은 월 수십만 행 수준에서 대체로 괜찮습니다. 월간 수천만 행이 들어온다면 주간 또는 일간 파티션이 인덱스를 더 작게 유지하고 VACUUM 작업을 분리하는 데 유리합니다.
-
파티션화된 테이블의 키와 제약 조건을 설계하세요. PostgreSQL에서는 고유 제약이 파티션 키를 포함해야 하는 경우가 있습니다. 흔한 패턴은 파티션 키로
created_at을 사용하고id를 함께 포함한(created_at, id)같은 복합 제약을 두는 것입니다. 이렇게 하면 나중에 기대했던 제약이 허용되지 않아 당황하는 일을 피할 수 있습니다. -
미래 파티션을 필요 전에 만드세요. 파티션이 없어 삽입이 실패하지 않도록 미리 생성하세요. 예를 들어 2~3개월치 미래 파티션을 미리 만드는 루틴을 정해두면 안전합니다.
-
파티션별 인덱스는 작고 의도적으로 유지하세요. 파티셔닝이 인덱스를 공짜로 만들어주지는 않습니다. 대부분의 이벤트 테이블은 파티션 키 외에 행위자(
actor_id), 엔티티(entity_id), 이벤트 타입 같은 실제 관리자 필터에 맞는 1~2개의 인덱스를 필요로 합니다. "혹시 몰라" 식의 인덱스는 피하세요. 필요하면 새 파티션에 추가하고 오래된 파티션은 백필(backfill)하세요. -
보존은 행 삭제가 아니라 파티션 드롭을 중심으로 계획하세요. 예를 들어 180일을 보관한다면 오래된 파티션을 드롭하면 대량 DELETE보다 빠릅니다. 누가 언제 보존을 실행하는지, 그리고 검증 방법까지 문서화하세요.
작은 예
감사 테이블이 주당 500만 행을 받는다면 created_at 기반의 주간 파티션이 합리적 시작입니다. 8주치 미래 파티션을 만들고 파티션당 행위자 검색용 인덱스와 엔티티 ID용 인덱스 등 두 개의 인덱스를 유지하세요. 보존 기간이 끝나면 수백만 행을 삭제하는 대신 가장 오래된 주간 파티션을 드롭하세요.
내부 도구를 AppMaster로 만든다면 파티션 키와 제약을 일찍 결정해서 데이터 모델과 생성된 코드가 같은 전제를 따르도록 하는 것이 도움이 됩니다.
파티셔닝이 관리자 패널 필터에 미치는 영향
로그 테이블을 파티셔닝하면 관리자 패널 필터는 단순한 UI 요소가 아닙니다. 필터가 쿼리가 몇 개의 파티션을 건드릴지 또는 몇 달치를 스캔할지를 결정하는 주요 요소가 됩니다.
가장 실용적인 변화는: 이제 시간은 선택적일 수 없습니다. 사용자가 날짜 범위 없이 무차별 검색을 허용하면 PostgreSQL이 모든(또는 많은) 파티션을 확인해야 할 수 있습니다. 각 확인이 빠르더라도 많은 파티션을 여는 것은 오버헤드를 추가해 페이지가 느려지게 합니다.
실무 규칙: 로그와 감사 검색에는 시간 범위를 요구하고 기본값을 "최근 24시간"처럼 합리적으로 설정하세요. 누군가가 정말로 "전체 기간"을 원하면 의도적인 선택으로 만들고 느릴 수 있음을 경고하세요.
파티션 프루닝과 필터를 일치시키세요
파티션 프루닝은 WHERE 절이 PostgreSQL이 활용할 수 있는 형태로 파티션 키를 포함할 때만 도움됩니다. created_at BETWEEN X AND Y 같은 필터는 깔끔하게 프루닝됩니다. 타임스탬프를 날짜로 캐스팅하거나 컬럼을 함수로 감싸는 패턴, 또는 파티션 키와 다른 시간 필드를 사용하는 필터는 프루닝을 방해합니다.
각 파티션 내부에서는 인덱스가 사람들이 실제로 어떻게 필터하는지와 일치해야 합니다. 실무적으로 시간 플러스 하나의 조건(테넌트/워크스페이스, 사용자, 액션 타입, 엔티티 ID, 상태)이 자주 중요합니다.
정렬 및 페이징: 얕게 유지하세요
파티셔닝이 깊은 페이징 문제를 스스로 해결하지는 않습니다. 관리자 패널이 최신순 정렬에 사용자가 페이지 5000으로 건너뛴다면 깊은 OFFSET 기반 페이징은 여전히 많은 행을 건너뛰게 하여 느립니다.
로그에는 커서형 페이징이 더 적합합니다: "이 (타임스탬프,id) 이전의 이벤트를 불러오기"처럼요. 이렇게 하면 인덱스를 사용하고 큰 OFFSET을 건너뛰는 비용을 피할 수 있습니다.
프리셋도 도움이 됩니다. 몇 가지 옵션(최근 24시간, 최근 7일, 오늘, 어제, 사용자 지정 범위)을 제공하면 우발적인 "전체 스캔"을 줄이고 관리자 경험을 예측 가능하게 만듭니다.
흔한 실수와 함정
대부분의 파티셔닝 프로젝트는 단순한 이유로 실패합니다: 파티셔닝 자체는 작동하지만 쿼리와 관리자 UI가 그것과 맞지 않습니다. 파티셔닝이 효과를 발휘하려면 실제 필터와 실제 보존 규칙 주위로 설계하세요.
1) 잘못된 시간 컬럼으로 파티셔닝
파티션 프루닝은 WHERE 절이 파티션 키와 일치할 때만 발생합니다. 흔한 실수는 created_at으로 파티션을 나눈 뒤 관리자 패널이 event_time으로 필터링하는 경우입니다. 지원팀이 항상 "10:00~10:15 사이에 무슨 일이 있었나"를 묻는데 테이블이 수신 시간으로 파티션되어 있으면 기대보다 더 많은 데이터를 건드릴 수 있습니다.
2) 너무 많은 작은 파티션 생성
시간당(또는 더 작은) 파티션은 보기에는 깔끔하지만 오버헤드가 큽니다: 관리할 객체 증가, 쿼리 플래너의 작업 증가, 인덱스나 권한 누락 가능성 증가. 극단적인 쓰기량과 엄격한 보존 요구가 없다면 일간 또는 월간 파티션이 운영상 더 쉬운 경우가 많습니다.
3) 전역 고유성이 여전히 작동한다고 가정
파티셔닝된 테이블에는 제약이 있습니다: 일부 고유 인덱스는 파티션 키를 포함해야 전체 파티션에 대해 보장할 수 있습니다. event_id가 전역적으로 유일하다고 기대하면 놀랄 수 있습니다. 전역 고유 식별자가 필요하면 UUID를 사용하고 애플리케이션 레이어에서 강제하는 방법을 고려하세요.
4) 관리자 UI가 제한 없는 검색을 허용
관리자 패널이 친절한 검색 상자를 제공해서 필터 없이 실행되게 하면 파티션된 로그 테이블에서는 모든 파티션을 스캔하게 될 수 있습니다. 메시지 페이로드에 대한 자유 텍스트 검색은 특히 위험합니다. 가드레일을 추가하세요: 시간 범위를 필수로 하고 기본 범위를 제한하며 "전체 기간"은 의도된 선택으로 만드세요.
5) 보존 계획(및 파티션 관리 계획)이 없음
파티셔닝이 자동으로 보존 문제를 해결해주지는 않습니다. 정책이 없으면 오래된 파티션이 쌓여 저장이 지저분해지고 유지보수가 느려집니다.
간단한 운영 규칙이 보통 문제를 예방합니다: 원시 이벤트 보관 기간을 정의하고, 미래 파티션 생성과 오래된 파티션 삭제를 자동화하며, 인덱스를 일관되게 적용하고 파티션 수와 경계 날짜를 모니터링하며 가장 느린 관리자 필터를 현실적 데이터 볼륨으로 테스트하세요.
커밋하기 전에 확인할 빠른 체크리스트
파티셔닝은 감사 로그에 큰 이득을 줄 수 있지만 주기적인 작업이 늘어납니다. 스키마를 변경하기 전에 사람들이 실제로 테이블을 어떻게 사용하는지 점검하세요.
주요 고장 지점에 대한 짧은 체크리스트:
- 시간 범위가 기본 필터인지 확인하세요. 대부분의 관리자 쿼리는 명확한 창(from/to)을 포함합니다. 무제한 검색이 흔하면 파티션 프루닝의 효과가 줄어듭니다.
- 보존은 행 삭제가 아니라 파티션 드롭으로 강제하세요. 오래된 파티션을 드롭하는 데 불편함이 없는지 확실히 하세요.
- 파티션 수가 합리적인지 추정하세요. 연간 파티션 수(일간/주간/월간)를 계산해 너무 많지도, 너무 적지도 않게 하세요.
- 인덱스가 실제 사용 필터와 일치하는지 확인하세요. 파티션 키 외에 공통 필터와 정렬 순서를 지원하는 인덱스를 파티션별로 유지하세요.
- 파티션이 자동으로 생성되고 모니터링되는지 확인하세요. 미래 파티션을 생성하는 작업이 실패하면 알 수 있도록 하세요.
실용적 테스트: 지원 또는 운영팀이 가장 자주 사용하는 세 가지 필터를 살펴보세요. 그중 두 가지가 보통 "시간 범위 + 하나의 조건"으로 충족된다면 이벤트 테이블의 PostgreSQL 파티셔닝은 상당히 고려할 가치가 있습니다.
현실적인 예와 다음 단계
지원팀이 하루 종일 두 화면을 켜 둡니다: "로그인 이벤트"(성공 및 실패 로그인)와 "보안 감사"(비밀번호 재설정, 역할 변경, API 키 업데이트). 고객이 의심스러운 활동을 신고하면 팀은 사용자로 필터하고 지난 몇 시간 기록을 확인해 짧은 보고서를 내보냅니다.
파티셔닝 전에는 모든 것이 하나의 큰 events 테이블에 들어 있습니다. 빠르게 성장하고 단순한 검색도 오래된 많은 행을 처리해야 해서 느려집니다. 보존도 고통스럽습니다: 야간 작업이 오래된 행을 삭제하지만 대량 삭제는 오래 걸리고 블로트를 만들며 일반 트래픽과 경쟁합니다.
이벤트 타임스탬프 기준으로 월별 파티셔닝을 적용하면 워크플로가 개선됩니다. 관리자 패널이 시간 필터를 요구하므로 대부분의 쿼리는 한두 개 파티션만 건드립니다. PostgreSQL이 선택된 범위 외의 파티션을 무시하므로 페이지 로딩이 빨라집니다. 보존은 수백만 행을 삭제하는 대신 오래된 파티션을 드롭하는 것으로 루틴화됩니다.
여전히 어려운 한 가지는 "전체 기간"에 대한 자유 텍스트 검색입니다. 누군가 날짜 제한 없이 IP 주소나 모호한 문구로 검색하면 파티셔닝으로는 싸게 만들 수 없습니다. 보통 제품 쪽에서 해결합니다: 검색 기본값을 시간 창으로 두고 "최근 24시간 / 7일 / 30일" 같은 경로를 눈에 띄게 만드는 것입니다.
실무에 도움이 되는 다음 단계:
- 관리자 패널 필터를 먼저 맵하세요. 사람들이 어떤 필드를 사용하고 어떤 필드를 필수로 삼아야 하는지 적어보세요.
- 탐색 방식에 맞춰 파티션을 선택하세요. 월간 파티션이 보통 좋은 출발점이며, 볼륨 때문에 필요할 때 주간으로 이동하세요.
- 시간 범위를 일급 시민으로 만드세요. UI에서 "날짜 없음"을 허용하면 느린 페이지를 기대하세요.
- 인덱스를 실제 필터와 맞추세요. 시간이 항상 포함된다면 시간 먼저인 인덱스 전략이 기본선이 됩니다.
- 보존 규칙을 파티션 경계에 맞추세요(예: 13개월 보관 후 드롭).
AppMaster(appmaster.io)로 내부 관리자 패널을 만든다면 이런 가정을 일찍 모델링하는 것이 가치 있습니다: 시간 기반 필터를 단순한 UI 선택이 아니라 데이터 모델의 일부로 취급하세요. 이 작은 결정이 로그 볼륨이 커질 때 쿼리 성능을 보호합니다.
자주 묻는 질문
파티셔닝은 일반적으로 일반 쿼리가 시간 범위로 제한되는 경우(예: "최근 24시간", "지난 7일")와 테이블이 커져 인덱스나 유지보수가 부담이 될 때 가장 유용합니다. 반면에 주요 쿼리가 "사용자 X의 전체 기록"처럼 시간 제한이 없다면, UI에서 시간 필터를 강제하고 각 파티션에 적절한 인덱스를 추가하지 않는 한 파티셔닝은 오히려 관리 오버헤드를 늘릴 수 있습니다.
로그와 감사 데이터에는 보통 시간 단위 범위 파티셔닝(range by time)이 기본적으로 가장 적합합니다. 데이터가 시간순으로 들어오고, 쿼리가 시간 창으로 시작하며, 보존이 시간 기반인 경우가 많기 때문입니다. 리스트(list)나 해시(hash) 파티셔닝은 특수한 경우에만 유용하며, 감사 워크플로에서는 보존과 탐색을 더 어렵게 만드는 경우가 많습니다.
사람들이 가장 먼저, 거의 항상 사용하는 필드를 선택하세요. 대부분의 관리자 패널에서는 시간 범위 필터가 첫 번째이므로 시간 기반 파티셔닝이 가장 예측 가능한 선택입니다. 파티션 키를 바꾸는 것은 큰 마이그레이션이 될 수 있으니 장기적인 약속으로 간주하세요.
타임스탬프나 테넌트 식별자처럼 파티션 수가 관리 가능한 값만 사용하세요. user_id 같은 높은 카디널리티 키는 수천 개의 파티션을 만들 수 있어 쿼리 플래닝 오버헤드와 운영 복잡성이 커지므로 피하는 것이 좋습니다.
created_at는 데이터베이스에 기록된 시간이고, occurred_at은 이벤트가 실제로 발생한 시간입니다. 지연된 수신(오프라인 클라이언트, 재시도, 큐)이 빈번하다면 운영적 안정성을 위해 created_at으로 파티셔닝하고 occurred_at을 필터용 인덱스로 두는 것이 더 실용적일 수 있습니다. 반대로 이벤트 시간이 신뢰할 만하면 occurred_at으로 파티셔닝하는 것이 사용자 관점에서 더 자연스럽습니다. 많은 팀은 created_at으로 파티셔닝하고 occurred_at을 색인하는 절충안을 선택합니다.
네. 테이블을 파티셔닝한 경우 관리자 UI에서 시간 범위를 필수로 하는 것이 좋습니다. 시간 필터가 없으면 PostgreSQL이 여러 파티션을 확인해야 해서 결과가 느려질 수 있습니다. 기본값을 "최근 24시간" 같은 값으로 두고, "전체 기간"은 의도적인 선택으로 만들어 느린 쿼리임을 알리는 것이 안전합니다.
자주 발생하는 실수는 파티션 키에 함수를 적용하거나(예: 타임스탬프를 date로 캐스팅) 파티션 키와 다른 시간 컬럼으로 필터링하는 것입니다. 이런 패턴은 파티션 프루닝을 방해합니다. created_at BETWEEN X AND Y 같은 단순한 형태로 WHERE 절을 유지하면 프루닝이 잘 작동합니다.
로그 뷰에 대해 깊은 OFFSET 기반 페이징은 여전히 느립니다. 대신 커서형 페이징을 사용하세요(예: (timestamp, id) 기준으로 "이전 이벤트 더 불러오기"). 이렇게 하면 인덱스를 사용하고 성능을 안정적으로 유지할 수 있습니다.
PostgreSQL에서는 파티셔닝된 테이블에 대한 일부 고유 제약은 파티션 키를 포함해야 전체 파티션에 대해 강제할 수 있습니다. 그래서 전역 고유 id 제약을 기대하면 놀랄 수 있습니다. 실무적 패턴은 파티션 키를 포함한 복합 고유제약(예: (created_at, id))을 사용하거나, 외부용 고유 식별자가 필요하면 UUID를 사용하고 전역 고유성을 앱 레이어에서 관리하는 것입니다.
파티션을 드롭하는 것이 빠르고 대량 DELETE가 만드는 부하와 VACUUM 작업을 피할 수 있습니다. 핵심은 보존 규칙을 파티션 경계에 맞춰 정하고 이를 자동화하는 것입니다: 미래 파티션을 미리 만들고 만료된 파티션을 주기적으로 제거하세요. 자동화가 없으면 파티셔닝이 수동 업무로 변합니다.


