PostgreSQL 14 이상에서 선언적 테이블 파티셔닝 이해 및 구현
PostgreSQL은 오랫동안 강력하고 다재다능한 관계형 데이터베이스였지만, 데이터 세트가 커짐에 따라 방대한 테이블을 관리하고 쿼리하는 것이 심각한 문제가 될 수 있습니다. 성능이 저하되고, 유지 관리 작업이 번거로워지며, 전반적인 시스템 효율성이 떨어집니다. PostgreSQL 10은 이러한 문제를 해결하기 위한 네이티브 솔루션으로 선언적 파티셔닝을 도입했으며, 특히 PostgreSQL 14 이상 버전에서 그 기능이 계속 성숙해지고 있습니다.
선언적 파티셔닝을 사용하면 큰 테이블을 파티션이라고 하는 더 작고 관리하기 쉬운 조각으로 나눌 수 있습니다. 이 전략은 데이터베이스가 관련 파티션만 스캔하도록 하여 쿼리 성능을 향상시킬 뿐만 아니라, 데이터 보관, 삭제, 인덱스 관리와 같은 유지 관리 작업을 단순화합니다. 이 문서는 PostgreSQL의 선언적 파티셔닝 핵심 개념을 이해하고, 다양한 유형을 탐색하며, 데이터베이스 최적화를 위해 이를 구현하는 방법에 대한 실용적인 예제를 제공하여 안내할 것입니다.
선언적 테이블 파티셔닝이란 무엇인가?
선언적 파티셔닝은 정의된 규칙 집합을 기반으로 단일 논리 테이블(부모 또는 파티션된 테이블)을 여러 물리적 테이블(자식 또는 파티션 테이블)로 분할할 수 있게 해주는 데이터베이스 기능입니다. 각 파티션은 부모 테이블의 데이터 하위 집합을 보유합니다. 파티셔닝 키는 행이 속할 파티션을 결정합니다.
선언적 파티셔닝의 주요 이점은 다음과 같습니다.
- 향상된 쿼리 성능: 파티셔닝 키로 필터링하는 쿼리는 PostgreSQL이 관련 데이터가 포함되지 않은 파티션을 가지치기(제거)할 수 있으므로 훨씬 빨라질 수 있습니다. 이 프로세스를 파티션 가지치기(partition pruning)라고 합니다.
- 용이한 데이터 관리: 오래된 데이터를 삭제하거나 보관하는 작업은 단일 대형 테이블에 대해 대규모
DELETE작업을 수행하는 대신 개별 파티션을 분리하거나 삭제하여 훨씬 효율적으로 수행할 수 있습니다. - 단순화된 유지 관리: 인덱싱 및 VACUUM 작업은 파티션별로 관리할 수 있어 전체 테이블에 미치는 영향을 줄입니다.
- 향상된 가용성: 개별 파티션에 대한 유지 관리는 전체 테이블에 대한 중단을 최소화하면서 수행될 수 있는 경우가 많습니다.
선언적 파티셔닝 유형
PostgreSQL은 데이터 분산 패턴에 적합한 여러 선언적 파티셔닝 방법을 지원합니다.
1. 범위 파티셔닝 (Range Partitioning)
범위 파티셔닝은 특정 열(예: 날짜, 숫자)의 연속적인 값 범위를 기반으로 데이터를 분할합니다.
사용 사례: 로그, 이벤트 데이터 또는 판매 기록과 같이 특정 날짜 또는 숫자 범위 내의 데이터를 자주 쿼리하는 시계열 데이터에 이상적입니다.
예시: sale_date 열을 기준으로 sales 테이블 파티셔닝.
범위 파티션 테이블 생성
먼저, 파티셔닝 방법과 키를 지정하여 부모 테이블을 생성합니다.
CREATE TABLE sales (
sale_id SERIAL,
product_name VARCHAR(100),
sale_amount NUMERIC(10, 2),
sale_date DATE NOT NULL
)
PARTITION BY RANGE (sale_date);
다음으로 개별 파티션을 생성합니다. 각 파티션은 포함할 범위를 지정하는 FOR VALUES 절로 정의됩니다.
-- 2023년 1월 판매에 대한 파티션
CREATE TABLE sales_2023_01
PARTITION OF sales ()
FOR VALUES FROM ('2023-01-01') TO ('2023-01-31');
-- 2023년 2월 판매에 대한 파티션
CREATE TABLE sales_2023_02
PARTITION OF sales ()
FOR VALUES FROM ('2023-02-01') TO ('2023-02-28');
-- 2023년 3월 판매에 대한 파티션
CREATE TABLE sales_2023_03
PARTITION OF sales ()
FOR VALUES FROM ('2023-03-01') TO ('2023-03-31');
팁: 범위를 정의할 때는 연속적이어야 하며 가능한 모든 값을 포함하도록 하십시오. 겹치는 범위를 피하십시오. TO 값은 배타적입니다.
2. 리스트 파티셔닝 (List Partitioning)
리스트 파티셔닝은 열에 있는 이산 값 목록을 기반으로 데이터를 분할합니다.
사용 사례: 지리적 영역, 상태 코드 또는 제품 카테고리와 같이 고정되고 알려진 값 집합이 있는 열에 적합합니다.
예시: region 열을 기준으로 orders 테이블 파티셔닝.
리스트 파티션 테이블 생성
PARTITION BY LIST로 부모 테이블을 정의합니다.
CREATE TABLE orders (
order_id SERIAL,
customer_name VARCHAR(100),
order_total NUMERIC(10, 2),
region VARCHAR(50) NOT NULL
)
PARTITION BY LIST (region);
특정 지역에 대한 파티션 생성:
-- 'North America' 지역 주문에 대한 파티션
CREATE TABLE orders_north_america
PARTITION OF orders ()
FOR VALUES IN ('North America');
-- 'Europe' 지역 주문에 대한 파티션
CREATE TABLE orders_europe
PARTITION OF orders ()
FOR VALUES IN ('Europe');
-- 'Asia' 지역 주문에 대한 파티션
CREATE TABLE orders_asia
PARTITION OF orders ()
FOR VALUES IN ('Asia');
중요: IN 목록과 일치하는 region에 대한 값을 삽입하거나 DEFAULT 파티션이 없는 경우 삽입이 실패합니다. 다른 모든 값을 포착하기 위해 DEFAULT 파티션을 만들 수 있습니다.
기본(Default) 파티션 생성
-- 명시적으로 나열되지 않은 모든 지역에 대한 기본 파티션
CREATE TABLE orders_other
PARTITION OF orders ()
DEFAULT;
3. 해시 파티셔닝 (Hash Partitioning)
해시 파티셔닝은 파티셔닝 키의 해시 값을 기반으로 데이터를 여러 파티션으로 분산시킵니다.
사용 사례: 데이터 볼륨이 크고 명확한 범위 또는 목록 기반 분산 없이 파티션에 고르게 분산시키려는 경우 유용합니다. 부하 분산에 좋습니다.
예시: user_id를 기준으로 users 테이블 파티셔닝.
해시 파티션 테이블 생성
PARTITION BY HASH로 부모 테이블을 정의하고 파티션 수를 지정합니다.
CREATE TABLE users (
user_id BIGSERIAL,
username VARCHAR(50) NOT NULL,
email VARCHAR(100)
)
PARTITION BY HASH (user_id);
지정하지 않으면 PostgreSQL이 자동으로 파티션을 생성하지만, 특히 파티션 수와 이름에 대한 제어를 원하는 경우 명시적으로 생성하는 것이 좋습니다.
명시적 해시 파티션 생성
-- 4개의 해시 파티션 생성
CREATE TABLE users_p0
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 0);
CREATE TABLE users_p1
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 1);
CREATE TABLE users_p2
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 2);
CREATE TABLE users_p3
PARTITION OF users
FOR VALUES WITH (modulus 4, remainder 3);
참고: 해시 파티셔닝을 사용할 때는 modulus(총 파티션 수)와 remainder(이 파티션의 나머지 값)를 지정해야 합니다.
선언적 파티셔닝 구현: 모범 사례
- 올바른 파티셔닝 키 선택: 파티셔닝 키는 가장 빈번한 쿼리 필터 및 데이터 관리 작업과 일치해야 합니다. 좋은 키는 성능을 크게 향상시킵니다.
- 파티션 수 고려: 파티션이 너무 적으면 이점이 충분하지 않을 수 있고, 너무 많으면 오버헤드가 증가할 수 있습니다. 관리 용이성과 성능의 균형을 맞출 수 있는 수를 목표로 하십시오. 범위 파티셔닝의 경우 데이터 성장률 및 보존 정책을 고려하십시오.
- 자동화를 위한
pg_partman사용: 특히 시계열 데이터의 범위 파티셔닝의 경우pg_partman과 같은 확장을 고려하십시오. 새 파티션 생성과 오래된 파티션의 보관/삭제를 자동화하여 수동 작업을 크게 줄여줍니다. - 전략적 인덱싱: 자식 테이블의 인덱스는 독립적입니다. 필요에 따라 개별 파티션에 인덱스를 생성할 수 있습니다. 효율적인 가지치기를 위해 파티셔닝 키에 인덱스를 생성하는 것을 고려하십시오.
- 파티션 가지치기:
WHERE절에 파티셔닝 키를 포함하여 쿼리를 작성하여 파티션 가지치기를 활용하도록 하십시오.EXPLAIN명령을 사용하여 가지치기가 발생하는지 확인할 수 있습니다. DEFAULT파티션: 리스트 파티셔닝의 경우 예상치 못한 새 값이 나타날 때 삽입 오류를 방지하기 위해DEFAULT파티션이 중요합니다.- 데이터 유형: 파티셔닝 키의 데이터 유형이 부모 테이블과 자식 테이블 전체에서 적절하고 일관적인지 확인하십시오.
파티션 관리
파티션 분리 및 결합
파티션은 CREATE TABLE ... PARTITION OF ...를 통해 직접 생성되지만, 기존 테이블을 분리하고 파티션으로 결합할 수도 있습니다. 이는 데이터 마이그레이션이나 대용량 데이터 세트 관리에 유용합니다.
파티션 분리: 파티션을 분리하려면 먼저 일반 테이블로 만든 다음 부모 테이블에서 분리해야 합니다. 최신 PostgreSQL 버전에서는 직접 분리할 수 있습니다.
-- sales_2023_01 파티션 분리
ALTER TABLE sales DETACH PARTITION sales_2023_01;
테이블을 파티션으로 결합: 스키마가 부모와 일치하는 일반 테이블을 새 파티션으로 결합할 수 있습니다.
-- 'old_sales_data'가 'sales'와 동일한 스키마를 가진 일반 테이블이라고 가정
CREATE TABLE sales_2022_12
PARTITION OF sales ()
FOR VALUES FROM ('2022-12-01') TO ('2022-12-31');
-- 기존 테이블을 새 파티션 슬롯에 결합
ALTER TABLE sales ATTACH PARTITION sales_2022_12
FOR VALUES FROM ('2022-12-01') TO ('2022-12-31');
-- 미리 생성된 테이블이 있는 경우 먼저 파티션으로 만들어야 합니다:
-- CREATE TABLE sales_2022_12 (LIKE sales INCLUDING ALL);
-- ... sales_2022_12 채우기 ...
-- ALTER TABLE sales ATTACH PARTITION sales_2022_12 FOR VALUES FROM ('2022-12-01') TO ('2022-12-31');
파티션 삭제
파티션을 삭제하는 것은 파티션 테이블만 제거하고 해당 내부 데이터는 제거하지 않기 때문에(명시적으로 지정하지 않는 한) 빠른 작업입니다. 이는 DELETE보다 훨씬 빠릅니다.
-- 파티션을 삭제하려면 자식 테이블을 삭제하면 됩니다
DROP TABLE sales_2023_01;
예시: 파티션 가지치기를 통한 쿼리 성능 향상
앞서 설명한 대로 sale_date를 기준으로 파티션된 sales 테이블을 고려해 봅시다.
파티션 가지치기가 없는 쿼리(파티션되지 않은 테이블에 대한 가상 쿼리):
SELECT SUM(sale_amount)
FROM sales
WHERE sale_date >= '2023-01-15' AND sale_date < '2023-01-20';
만약 sales가 거대한 비파티션 테이블이었다면, 이 쿼리는 전체 테이블을 스캔했을 것입니다. 그러나 선언적 파티셔닝을 사용하면 다음과 같습니다.
-- 이 쿼리는 sales_2023_01 파티션만 스캔합니다
SELECT SUM(sale_amount)
FROM sales
WHERE sale_date >= '2023-01-15' AND sale_date < '2023-01-20';
PostgreSQL의 쿼리 플래너는 sale_date가 파티셔닝 키이며 지정된 범위가 sales_2023_01 파티션 내에 완전히 포함됨을 인식합니다. 따라서 해당 파티션만 스캔하여 I/O를 크게 줄이고 성능을 향상시킵니다.
이를 확인하려면 EXPLAIN을 사용하십시오.
EXPLAIN SELECT SUM(sale_amount) FROM sales WHERE sale_date >= '2023-01-15' AND sale_date < '2023-01-20';
출력에는 관련 없는 파티션이 제외되었음을 나타내는 PartitionPrune 단계가 표시됩니다.
결론
PostgreSQL 14 이상에서의 선언적 파티셔닝은 대규모 데이터 세트를 관리하고 최적화하기 위한 강력한 기능입니다. 범위, 리스트 또는 해시 전략을 기반으로 테이블을 지능적으로 분할함으로써 쿼리 성능, 데이터 관리 효율성 및 전반적인 데이터베이스 유지 관리 측면에서 상당한 개선을 이룰 수 있습니다. 사용 가능한 파티셔닝 유형을 이해하고 구현 시 모범 사례를 적용하는 것이 애플리케이션에서 이 기능의 잠재력을 최대한 발휘하는 열쇠가 될 것입니다.