대용량 PostgreSQL 테이블의 선언적 파티셔닝 모범 사례

선언적 파티셔닝을 활용하여 대용량 PostgreSQL 테이블을 최적화하세요. 이 가이드는 범위, 목록, 해시 파티셔닝 전략을 탐구하고, 키 선택, 파티션 관리, 인덱싱 및 쿼리 성능 향상을 위한 모범 사례를 제공합니다. 유지 관리 오버헤드를 줄이고 대규모 데이터 세트를 효율적으로 처리하여 더 빠르고 확장성 있는 데이터베이스 작업을 수행하는 방법을 배우세요.

33 조회수

대규모 PostgreSQL 테이블의 선언적 파티셔닝 모범 사례

대규모 PostgreSQL 테이블은 심각한 성능 병목 현상이 될 수 있습니다. 데이터 세트가 증가함에 따라 INSERT, UPDATE, DELETE, SELECT 쿼리와 같은 작업 속도가 상당히 느려져 애플리케이션 반응성 및 사용자 경험에 영향을 미칠 수 있습니다. 버전 11부터 도입된 PostgreSQL의 선언적 파티셔닝(declarative partitioning)은 이러한 대규모 테이블을 파티션이라고 불리는 더 작고 관리하기 쉬운 조각으로 나누어 관리하는 강력한 솔루션을 제공합니다. 이 접근 방식은 올바르게 구현될 경우 상당한 성능 향상, 유지보수 오버헤드 감소 및 효율적인 데이터 관리로 이어질 수 있습니다.

이 문서는 PostgreSQL에서 선언적 파티셔닝을 구현하기 위한 모범 사례를 안내합니다. 다양한 파티셔닝 전략(범위, 목록 및 해시)을 살펴보고, 대규모 데이터 세트의 최적 성능과 관리 용이성을 위해 이 기능을 활용하는 데 도움이 되는 실용적인 예제와 권장 사항을 제공할 것입니다.

선언적 파티셔닝 이해

선언적 파티셔닝을 사용하면 테이블을 파티션으로 정의하고, 파티셔닝 키와 전략을 지정할 수 있습니다. 그러면 PostgreSQL은 파티셔닝 키의 값에 따라 데이터를 적절한 파티션으로 자동으로 라우팅합니다. 이를 통해 복잡한 트리거 또는 수동 데이터 관리가 필요 없으므로, 이전 방법에 비해 훨씬 깔끔하고 효율적인 솔루션을 제공합니다.

선언적 파티셔닝의 주요 이점:

  • 향상된 쿼리 성능: 파티셔닝 키로 필터링하는 쿼리는 관련 파티션만 스캔하여 처리되는 데이터 양을 획기적으로 줄일 수 있습니다.
  • 더 빠른 데이터 로드: 대량 로드 작업은 특정 파티션으로 지정될 수 있어 효율성이 향상됩니다.
  • 단순화된 유지보수: 아카이빙, 오래된 데이터 삭제 또는 인덱스 재구성과 같은 작업은 전체 테이블에 영향을 주지 않으면서 개별 파티션에서 수행할 수 있습니다.
  • 오버헤드 감소: 수동 파티셔닝 로직 및 관련 유지보수 필요성이 사라집니다.

PostgreSQL의 파티셔닝 전략

PostgreSQL은 선언적 파티셔닝을 위한 세 가지 주요 전략을 제공하며, 각각 다른 사용 사례에 적합합니다.

1. 범위 파티셔닝 (Range Partitioning)

범위 파티셔닝은 파티셔닝 키의 연속적인 값 범위를 기준으로 데이터를 나눕니다. 이는 시계열 데이터, 순차적 ID 또는 값이 정의된 간격 내에 있는 모든 데이터에 이상적입니다.

언제 사용해야 하는가:
* 시계열 데이터 (예: 날짜/타임스탬프별 로그, 이벤트).
* 순차적으로 생성된 ID.
* 정렬되고 연속적인 값을 가진 데이터.

예시: sale_date를 기준으로 sales 테이블을 파티셔닝합니다.

-- Create the parent partitioned table
CREATE TABLE sales (
    sale_id SERIAL,
    product_id INT,
    amount DECIMAL(10, 2),
    sale_date DATE NOT NULL
)
PARTITION BY RANGE (sale_date);

-- Create partitions for specific date ranges
CREATE TABLE sales_2023_q1 PARTITION OF sales
    FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');

CREATE TABLE sales_2023_q2 PARTITION OF sales
    FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');

CREATE TABLE sales_2023_q3 PARTITION OF sales
    FOR VALUES FROM ('2023-07-01') TO ('2023-10-01');

CREATE TABLE sales_2023_q4 PARTITION OF sales
    FOR VALUES FROM ('2023-10-01') TO ('2024-01-01');

-- Inserting data automatically goes to the correct partition
INSERT INTO sales (product_id, amount, sale_date) VALUES (101, 150.50, '2023-02-15');

2. 목록 파티셔닝 (List Partitioning)

목록 파티셔닝은 파티셔닝 키의 이산적인 값 목록을 기준으로 데이터를 나눕니다. 이는 고정되고 알려진 범주 또는 식별자 집합이 있을 때 유용합니다.

언제 사용해야 하는가:
* 지리적 영역 (예: country, state).
* 제품 카테고리.
* 사용자 역할 또는 상태.

예시: country_code를 기준으로 customers 테이블을 파티셔닝합니다.

-- Create the parent partitioned table
CREATE TABLE customers (
    customer_id SERIAL,
    name VARCHAR(100),
    country_code CHAR(2) NOT NULL
)
PARTITION BY LIST (country_code);

-- Create partitions for specific country codes
CREATE TABLE customers_us PARTITION OF customers
    FOR VALUES IN ('US');

CREATE TABLE customers_ca PARTITION OF customers
    FOR VALUES IN ('CA');

CREATE TABLE customers_uk PARTITION OF customers
    FOR VALUES IN ('GB');

-- Inserting data automatically goes to the correct partition
INSERT INTO customers (name, country_code) VALUES ('John Doe', 'US');

3. 해시 파티셔닝 (Hash Partitioning)

해시 파티셔닝은 파티셔닝 키의 해시 값을 기준으로 데이터를 나눕니다. 이는 자연스러운 범위나 목록이 없을 때 데이터를 파티션 전체에 걸쳐 균등하게 분산하여 I/O 부하의 균형을 맞추는 데 유용합니다.

언제 사용해야 하는가:
* 다른 전략이 적합하지 않을 때 데이터를 균등하게 분산하는 경우.
* I/O 핫스팟을 방지하는 경우.
* 균등 분산이 중요한 대용량 트랜잭션 테이블.

예시: order_id를 기준으로 orders 테이블을 파티셔닝합니다.

-- Create the parent partitioned table
CREATE TABLE orders (
    order_id BIGSERIAL,
    user_id INT,
    order_total DECIMAL(10, 2)
)
PARTITION BY HASH (order_id);

-- Create a specified number of partitions (e.g., 4)
CREATE TABLE orders_part_1 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE orders_part_2 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE orders_part_3 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE orders_part_4 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 3);

-- Inserting data automatically goes to the correct partition
INSERT INTO orders (user_id, order_total) VALUES (500, 250.75);

선언적 파티셔닝 구현 모범 사례

파티셔닝을 효과적으로 구현하려면 이점을 극대화하기 위해 신중한 계획과 모범 사례 준수가 필요합니다.

1. 올바른 파티셔닝 키 선택

파티셔닝 키는 가장 중요한 결정 사항입니다. 이는 쿼리 성능과 유지보수에 직접적인 영향을 미칩니다. 가장 일반적인 쿼리의 WHERE 절에서 자주 사용되는 키를 선택하십시오.

  • 시계열 데이터의 경우: DATE, TIMESTAMP 열은 범위 파티셔닝을 위한 훌륭한 후보입니다.
  • 범주형 데이터의 경우: country_code, status, region과 같은 열은 목록 파티셔닝에 적합합니다.
  • 균등 분산을 위해: 쿼리에서 자주 사용되고 카디널리티가 높은 열이 해시 파티셔닝에 적합합니다.

팁: WHERE 절에서 거의 사용되지 않거나 파티션 간에 고유한 값이 없는 열을 기준으로 파티셔닝하는 것을 피하십시오. 이는 쿼리가 모든 파티션을 스캔하게 만들 수 있습니다.

2. 적절한 파티셔닝 전략 선택

앞서 논의했듯이, 데이터와 쿼리 패턴에 가장 적합한 전략(범위, 목록, 해시)을 선택하십시오.

  • 범위 (Range): 정렬되고 연속적인 데이터에 적합합니다.
  • 목록 (List): 이산적이고 알려진 범주에 적합합니다.
  • 해시 (Hash): 균등한 데이터 분산 및 로드 밸런싱에 적합합니다.

3. 파티션 크기와 개수 계획

파티션 크기에 대한 만능의 정답은 없습니다. 하지만 다음 사항을 고려하십시오.

  • 너무 많은 작은 파티션: 플래너와 시스템에 대한 오버헤드가 증가할 수 있습니다. 각 파티션에는 자체 메타데이터가 있습니다.
  • 너무 적은 큰 파티션: 파티셔닝의 성능 이점을 상쇄할 수 있습니다.
  • 이상적인 크기: 성능상의 이점을 제공할 만큼 충분히 크지만 유지보수 작업에 관리하기 쉬운 파티션을 목표로 하십시오. 일반적인 시작점은 파티션을 논리적 시간 단위(예: 시계열 데이터의 경우 일별, 주별, 월별) 또는 관리 가능한 데이터 볼륨에 맞추는 것입니다.

팁: 파티션 크기를 모니터링하고 데이터 증가에 따라 파티셔닝 전략을 조정하십시오. 필요한 경우 파티션을 분리했다가 다시 연결하거나 다른 전략으로 파티션을 다시 생성할 수도 있습니다.

4. 향후 데이터에 대한 파티셔닝 전략 정의

파티션된 테이블을 생성할 때, 기존 파티션에 속하지 않는 데이터를 처리하기 위한 기본 파티션(default partitions) 또는 전략을 정의할 수도 있습니다. 그러나 예기치 않은 데이터 배치나 오류를 방지하기 위해 일반적으로 파티션을 명시적으로 생성하는 것이 좋습니다.

예시: 해시 파티셔닝에 DEFAULT 파티션 사용 (주의해서 사용하고 데이터 관리에 미치는 영향을 고려하십시오).

-- This is an example for PostgreSQL 14+ for default partitions
-- CREATE TABLE orders (
--     order_id BIGSERIAL,
--     user_id INT,
--     order_total DECIMAL(10, 2)
-- )
-- PARTITION BY HASH (order_id);
-- CREATE TABLE orders_part_1 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 0);
-- CREATE TABLE orders_part_2 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 1);
-- CREATE TABLE orders_part_3 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 2);
-- CREATE TABLE orders_part_4 PARTITION OF orders FOR VALUES WITH (MODULUS 4, REMAINDER 3);
-- CREATE TABLE orders_default PARTITION OF orders DEFAULT;

모범 사례: 명확성과 제어를 위해 예상되는 데이터 범위/목록에 대해 파티션을 수동으로 생성하십시오. 특히 목록 또는 범위 파티셔닝의 경우 의도하지 않은 데이터가 누적될 수 있으므로 DEFAULT 파티션은 신중하게 고려하십시오.

5. 파티션 수명 주기 관리 (데이터 아카이빙/삭제)

파티셔닝의 가장 큰 장점 중 하나는 데이터 수명 주기 관리가 단순화된다는 것입니다. 시계열 데이터의 경우, 오래된 데이터를 아카이브하거나 삭제하는 것이 일반적입니다.

  • 파티션 분리 (Detaching Partitions): 다른 파티션에 영향을 주지 않으면서 해당 데이터를 아카이브하거나 완전히 삭제하기 위해 파티션을 분리할 수 있습니다.

    ```sql
    -- Detach a partition
    ALTER TABLE sales DETACH PARTITION sales_2023_q1;

    -- Optionally, archive the detached partition before dropping
    -- CREATE TABLE sales_archive_2023_q1 (LIKE sales INCLUDING ALL);
    -- INSERT INTO sales_archive_2023_q1 SELECT * FROM sales_2023_q1;

    -- Drop the detached partition
    DROP TABLE sales_2023_q1;
    ```

  • 파티션 삭제 (Dropping Partitions): 더 이상 쿼리할 필요가 없는 매우 오래된 데이터의 경우.

    sql -- Directly drop a partition (if not detached first, the parent table needs to know) DROP TABLE sales_2023_q1;

팁: cron 작업 또는 기타 스케줄링 도구(종종 스크립트와 결합)를 사용하여 새 파티션 생성과 오래된 파티션 분리/삭제 작업을 자동화하십시오.

6. 파티션에 대한 인덱싱

파티션된 테이블의 인덱스는 부모 테이블 수준 또는 개별 파티션 수준에서 관리될 수 있습니다.

  • 글로벌 인덱스 (Global Indexes): 부모 테이블에 정의됩니다. 이는 모든 파티션에서 유지 관리됩니다. 편리할 수 있지만 삽입 시 오버헤드가 더 높고 로컬 인덱스보다 느릴 수 있습니다.
  • 로컬 인덱스 (Local Indexes): 개별 파티션에 정의됩니다. 일반적으로 INSERT 작업에 더 빠르며 특정 파티션을 대상으로 하는 쿼리에 더 효율적일 수 있습니다. 각 파티션은 자체 인덱스를 갖게 됩니다.

모범 사례: 대부분의 사용 사례에서 더 나은 성능과 관리 용이성을 위해 로컬 인덱스가 권장됩니다. 로컬 인덱스는 독립적인 관리를 허용하고 더 효율적일 수 있습니다. 파티션되지 않은 테이블에서 사용할 인덱싱 전략을 반영하는 인덱스를 파티션에 생성하십시오.

-- Example: Creating a local index on a partition
CREATE INDEX ON sales_2023_q2 (product_id);

7. PARTITION BYPARTITION OF 구문 진화 고려

PostgreSQL은 파티션된 테이블을 생성하기 위한 구문이 발전해 왔습니다. 사용 중인 PostgreSQL 버전에 적합한 구문을 사용하고 있는지 확인하십시오. 버전 11부터 부모 테이블에 PARTITION BY를, 자식 테이블에 PARTITION OF ... FOR VALUES를 사용하는 것이 표준 선언적 접근 방식입니다.

8. 쿼리 계획 모니터링 및 분석

파티셔닝을 구현한 후에는 쿼리 성능을 모니터링하는 것이 중요합니다. EXPLAIN ANALYZE를 사용하여 쿼리가 파티션을 올바르게 가지치기(pruning)하는지(즉, 관련 파티션만 스캔하는지) 확인하십시오.

EXPLAIN ANALYZE SELECT * FROM sales WHERE sale_date BETWEEN '2023-02-01' AND '2023-02-28';

쿼리 플래너가 sales_2023_q1 파티션만 고려하고 있다는 표시가 EXPLAIN 출력에 나타나는지 확인하십시오. 쿼리 계획이 불필요하게 여러 파티션 또는 모든 파티션을 스캔하는 것을 보여준다면, 파티셔닝 키나 쿼리 조정이 필요할 수 있습니다.

고급 고려 사항

외래 키 및 고유 제약 조건

  • 외래 키 (Foreign Keys): 외래 키 제약 조건은 부모 파티션 테이블이 아닌 리프 파티션(leaf partitions)에만 정의할 수 있습니다. 즉, 관련 파티션 각각에 FK를 정의해야 합니다.
  • 고유 제약 조건 (Unique Constraints): 외래 키와 마찬가지로 고유 제약 조건은 리프 파티션에만 정의할 수 있습니다. 전체 테이블에서 고유성을 강제하려면 각 파티션에 파티셔닝 키 자체에 대한 고유 제약 조건을 정의해야 하며, 잠재적으로 파티셔닝 키를 포함하는 부모 테이블에 UNIQUE INDEX를 사용해야 합니다.

팁: 전체 테이블에 걸쳐 고유성을 확보하려면 리프 파티션의 고유 제약 조건에 파티셔닝 키를 추가하는 것을 고려하십시오. 예: country_code 목록 파티셔닝의 경우 UNIQUE (country_code, customer_id).

INSERT 성능

파티셔닝이 일반적으로 SELECT 성능을 향상시키지만, INSERT 성능에는 영향을 미칠 수 있습니다. 파티셔닝 키가 균일하게 분산되지 않거나 파티셔닝 로직이 복잡한 경우, PostgreSQL이 올바른 파티션을 결정하는 데 일부 오버헤드가 발생할 수 있습니다. 해시 파티셔닝은 쓰기 부하를 분산하는 데 종종 좋습니다.

기존 대규모 테이블을 위한 파티셔닝 전략

기존의 매우 큰 테이블을 파티셔닝하는 것은 복잡한 작업일 수 있습니다. 여기에는 종종 다음이 포함됩니다:

  1. 새로운 파티션된 테이블 구조 생성.
  2. 이력 데이터에 대한 파티션 생성.
  3. 이전 테이블의 데이터를 새 파티션된 테이블로 복사 (배치로 수행 가능).
  4. 애플리케이션 읽기/쓰기를 새 파티션된 테이블로 전환.
  5. 이전 테이블 삭제.

이 과정은 가동 중지 시간을 최소화하기 위해 신중하게 계획하고, 스테이징 환경에서 테스트하며, 유지보수 기간 동안 실행해야 합니다.

결론

PostgreSQL의 선언적 파티셔닝은 대규모 데이터 세트를 관리하고 쿼리 성능을 향상시키는 강력한 기능입니다. 파티셔닝 키와 전략을 신중하게 선택하고 파티션을 효과적으로 관리함으로써 상당한 이점을 얻을 수 있습니다. 파티셔닝 스키마를 계획하고, 성능을 모니터링하며, 데이터가 진화함에 따라 전략을 조정하는 것을 잊지 마십시오. 이러한 모범 사례를 준수하면 PostgreSQL 데이터베이스가 확장되더라도 성능을 유지하고 관리하기 용이하도록 보장할 수 있습니다.