Custom Partitioning Key

Custom Partitioning Key (커스텀 파티셔닝 키)

대부분의 경우 파티션 키가 필요하지 않아요. 그리고 대부분의 다른 경우에는 일 단위 파티셔닝이 흔한 관측 가능성(observability) 사용 사례를 목표로 하지 않는 한, 월 단위보다 더 세밀한 파티션 키가 필요하지 않아요.

너무 세밀한 파티셔닝을 사용하면 안 돼요. 클라이언트 식별자나 이름으로 데이터를 파티셔닝하지 마세요. 대신 클라이언트 식별자나 이름을 ORDER BY 표현식의 첫 컬럼으로 만들어요.

파티셔닝은 MergeTree 패밀리 테이블, 복제 테이블, 그리고 materialized views에서 사용할 수 있어요.

파티션은 지정된 기준에 의한 테이블의 레코드 논리적 조합이에요. 월별, 일별, 이벤트 유형별 같은 임의의 기준으로 파티션을 설정할 수 있어요. 각 파티션은 이 데이터 조작을 단순화하기 위해 별도로 저장돼요. 데이터에 접근할 때 ClickHouse는 가능한 가장 작은 파티션 하위 집합을 사용해요. 파티션은 파티셔닝 키를 포함하는 쿼리의 성능을 향상시켜요. ClickHouse가 파티션 내 파트와 그레인(granule)을 선택하기 전에 그 파티션을 필터링하기 때문이에요.

파티션은 테이블 생성PARTITION BY expr 절에 지정돼요. 파티션 키는 테이블 컬럼의 어떤 표현식이든 될 수 있어요. 예를 들어 월 단위 파티셔닝을 지정하려면 toYYYYMM(date_column) 표현식을 사용해요.

CREATE TABLE visits
(
    VisitDate Date,
    Hour UInt8,
    ClientID UUID
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(VisitDate)
ORDER BY Hour;

파티션 키는 표현식의 튜플일 수도 있어요 (기본 키와 비슷). 예:

ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/tables/name', 'replica1', Sign)
PARTITION BY (toMonday(StartDate), EventType)
ORDER BY (CounterID, StartDate, intHash32(UserID));

이 예시에서 현재 주에 발생한 이벤트 유형별로 파티셔닝을 설정했어요. 기본적으로 부동소수점 파티션 키는 지원되지 않아요. 사용하려면 allow_floating_point_partition_key 설정을 활성화해요.

테이블에 새 데이터를 삽입하면 이 데이터는 기본 키로 정렬된 별도의 파트(청크)로 저장돼요. 삽입 후 10-15분 안에 같은 파티션의 파트들이 전체 파트로 병합돼요.

병합은 파티셔닝 표현식에 같은 값을 가진 데이터 파트에서만 동작해요. 이는 너무 세밀한 파티션을 만들면 안 된다는 뜻이에요 (약 천 개보다 많은 파티션). 그렇지 않으면 파일시스템에 비합리적으로 많은 파일과 열린 파일 디스크립터 때문에 SELECT 쿼리 성능이 나빠져요.

system.parts 테이블을 사용해 테이블 파트와 파티션을 볼 수 있어요. 예를 들어 월 단위 파티셔닝이 있는 visits 테이블이 있다고 가정해요. system.parts 테이블에 SELECT 쿼리를 수행해볼게요.

SELECT
    partition,
    name,
    active
FROM system.parts
WHERE table = 'visits'
┌─partition─┬─name──────────────┬─active─┐
│ 201901    │ 201901_1_3_1      │      0 │
│ 201901    │ 201901_1_9_2_11   │      1 │
│ 201901    │ 201901_8_8_0      │      0 │
│ 201901    │ 201901_9_9_0      │      0 │
│ 201902    │ 201902_4_6_1_11   │      1 │
│ 201902    │ 201902_10_10_0_11 │      1 │
│ 201902    │ 201902_11_11_0_11 │      1 │
└───────────┴───────────────────┴────────┘

partition 컬럼은 파티션의 이름을 포함해요. 이 예시에 201901201902 두 개의 파티션이 있어요. 이 컬럼 값을 ALTER … PARTITION 쿼리에서 파티션 이름을 지정하는 데 사용할 수 있어요.

name 컬럼은 파티션 데이터 파트의 이름을 포함해요. 이 컬럼을 ALTER ATTACH PART 쿼리에서 파트 이름을 지정하는 데 사용할 수 있어요.

파트 이름을 분해해볼게요: 201901_1_9_2_11:

  • 201901은 파티션 이름
  • 1은 데이터 블록의 최소 번호
  • 9는 데이터 블록의 최대 번호
  • 2는 청크 레벨 (이것이 형성된 병합 트리의 깊이)
  • 11은 mutation 버전 (파트가 mutation된 경우)

구형 테이블의 파트 이름은 20190117_20190123_2_2_0 (최소 날짜 - 최대 날짜 - 최소 블록 번호 - 최대 블록 번호 - 레벨) 형식이에요.

active 컬럼은 파트의 상태를 보여줘요. 1은 활성, 0은 비활성. 비활성 파트는 예를 들어 더 큰 파트로 병합된 후 남은 소스 파트예요. 손상된 데이터 파트도 비활성으로 표시돼요.

예시에서 볼 수 있듯이 같은 파티션의 여러 분리된 파트가 있어요 (예: 201901_1_3_1201901_1_9_2). 이는 이 파트들이 아직 병합되지 않았다는 뜻이에요. ClickHouse는 삽입된 데이터 파트를 대략 삽입 후 15분 주기로 병합해요. 또한 OPTIMIZE 쿼리로 예정되지 않은 병합을 수행할 수 있어요. 예:

OPTIMIZE TABLE visits PARTITION 201902;
┌─partition─┬─name─────────────┬─active─┐
│ 201901    │ 201901_1_3_1     │      0 │
│ 201901    │ 201901_1_9_2_11  │      1 │
│ 201901    │ 201901_8_8_0     │      0 │
│ 201901    │ 201901_9_9_0     │      0 │
│ 201902    │ 201902_4_6_1     │      0 │
│ 201902    │ 201902_4_11_2_11 │      1 │
│ 201902    │ 201902_10_10_0   │      0 │
│ 201902    │ 201902_11_11_0   │      0 │
└───────────┴──────────────────┴────────┘

비활성 파트는 병합 후 대략 10분에 삭제돼요.

파트와 파티션의 집합을 보는 또 다른 방법은 테이블의 디렉터리 /var/lib/clickhouse/data/<database>/<table>/로 가는 것이에요. 예:

/var/lib/clickhouse/data/default/visits$ ls -l
total 40
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  1 16:48 201901_1_3_1
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 16:17 201901_1_9_2_11
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 15:52 201901_8_8_0
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 15:52 201901_9_9_0
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 16:17 201902_10_10_0
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 16:17 201902_11_11_0
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 16:19 201902_4_11_2_11
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  5 12:09 201902_4_6_1
drwxr-xr-x 2 clickhouse clickhouse 4096 Feb  1 16:48 detached

'201901_1_1_0', '201901_1_7_1' 등의 폴더는 파트의 디렉터리예요. 각 파트는 해당 파티션에 속하고 특정 달(이 예시의 테이블은 월 단위 파티셔닝)의 데이터만 담아요.

detached 디렉터리는 DETACH 쿼리로 테이블에서 분리된 파트를 포함해요. 손상된 파트도 삭제되는 대신 이 디렉터리로 이동돼요. 서버는 detached 디렉터리의 파트를 사용하지 않아요. ATTACH 쿼리를 실행하기 전까지 서버는 이 디렉터리의 데이터를 추가, 삭제 또는 수정해도 알지 못해요.

운영 중인 서버에서는 파일시스템에서 파트 집합이나 데이터를 수동으로 변경할 수 없다는 점을 유의해요. 서버가 알지 못하기 때문이에요. 비복제 테이블의 경우 서버가 중지되었을 때 할 수 있지만 권장되지 않아요. 복제 테이블의 경우 파트 집합은 어떤 경우에도 변경할 수 없어요.

ClickHouse를 사용하면 파티션으로 연산할 수 있어요: 삭제, 한 테이블에서 다른 테이블로 복사, 또는 백업 생성. 모든 연산 목록은 Manipulations With Partitions and Parts 섹션에서 확인해요.

파티션 키를 사용한 Group By 최적화

테이블의 파티션 키와 쿼리의 group by 키의 일부 조합에서는 각 파티션에 대해 독립적으로 집계를 실행하는 것이 가능할 수 있어요. 그러면 마지막에 모든 실행 스레드의 부분 집계 데이터를 병합할 필요가 없어요. 각 group by 키 값이 두 개의 다른 스레드의 작업 집합에 나타날 수 없다는 보장을 받았기 때문이에요.

전형적인 예시:

CREATE TABLE session_log
(
    UserID UInt64,
    SessionID UUID
)
ENGINE = MergeTree
PARTITION BY sipHash64(UserID) % 16
ORDER BY tuple();

SELECT
    UserID,
    COUNT()
FROM session_log
GROUP BY UserID;

이런 쿼리의 성능은 테이블 레이아웃에 따라 달라져요. 이 최적화는 버전 26.7부터 기본적으로 활성화되며, 런타임 휴리스틱이 파티션 레이아웃이 불리할 때 — 구체적으로 파티션이 너무 적거나(max_threads / 2보다 적음), 너무 많거나(max_number_of_partitions_for_independent_aggregation보다 많음), 또는 파티션 크기가 심하게 치우쳤을 때(가장 큰 파티션이 총 행 수를 max_threads로 나눈 값의 두 배보다 많은 행을 보유) 자동으로 건너뛰어요. 아래 목록은 일반적으로 좋은 성능을 위한 레이아웃 요소를 설명해요. 이 중 파티션 수와 크기 치우침만 런타임 휴리스틱에 의해 강제돼요.

좋은 성능을 위한 핵심 요소:

  • 쿼리에 포함된 파티션 수가 충분히 커야 해요 (max_threads / 2보다 크게). 그렇지 않으면 쿼리가 머신을 충분히 활용하지 못해요
  • 파티션이 너무 작으면 안 돼요. 그래야 배치 처리가 행 단위 처리로 퇴화하지 않아요
  • 파티션 크기가 비슷해야 해요. 그래야 모든 스레드가 대략 같은 양의 작업을 해요

partition by 절의 컬럼에 일부 해시 함수를 적용해 파티션 간 데이터를 고르게 분배하는 것이 권장돼요.

관련 설정:

  • allow_aggregate_partitions_independently - 최적화 사용이 활성화되어 있는지 제어
  • force_aggregate_partitions_independently - 정확성 관점에서 적용 가능할 때 사용을 강제하지만, 그 타당성을 추정하는 내부 로직에 의해 비활성화될 수 있음
  • max_number_of_partitions_for_independent_aggregation - 테이블이 가질 수 있는 최대 파티션 수의 하드 리미트

더 알아보기 (Learn more)