저장 효율

저장 효율 (Storage efficiency)

ClickHouse에서 시계열 데이터의 저장 공간을 타입 최적화와 전문화된 코덱으로 절약하는 방법을 살펴봐요.

출처: 문서

본문

Wikipedia 통계 데이터셋을 조회하는 방법을 탐구한 뒤, ClickHouse에서 그 저장 효율을 최적화하는 데 초점을 맞춰 볼게요. 이 섹션은 쿼리 성능을 유지하면서 저장 요구 사항을 줄이는 실용적인 기법을 보여줘요.

타입 최적화

저장 효율을 최적화하는 일반적인 접근 방식은 최적의 데이터 타입을 사용하는 거예요. projectsubproject 컬럼을 살펴볼게요. 이 컬럼들은 String 타입이지만 고유 값 수가 상대적으로 적어요:

SELECT
    uniq(project),
    uniq(subproject)
FROM wikistat;
┌─uniq(project)─┬─uniq(subproject)─┐
│          1332 │              130 │
└───────────────┴──────────────────┘

이는 사전 기반 인코딩을 사용하는 LowCardinality() 데이터 타입을 사용할 수 있다는 뜻이에요. 이는 ClickHouse가 원래 문자열 값 대신 내부 값 ID를 저장하게 해서 많은 공간을 절약해요:

ALTER TABLE wikistat
MODIFY COLUMN `project` LowCardinality(String),
MODIFY COLUMN `subproject` LowCardinality(String)

hits 컬럼에는 8바이트를 차지하는 UInt64 타입도 사용했지만, 최대 값이 상대적으로 작아요:

SELECT max(hits)
FROM wikistat;
┌─max(hits)─┐
│    449017 │
└───────────┘

이 값을 감안하면 4바이트만 차지하고 최대 약 40억까지 저장할 수 있는 UInt32를 사용할 수 있어요:

ALTER TABLE wikistat
MODIFY COLUMN `hits` UInt32;

이러면 이 컬럼의 메모리 크기가 최소 2배 줄어들 거예요. 압축 덕분에 디스크의 크기는 변하지 않는다는 점에 유의하세요. 하지만 너무 작지 않은 데이터 타입을 선택하도록 주의하세요!

전문화된 코덱

시계열처럼 순차적인 데이터를 다룰 때 특수 코덱을 사용해 저장 효율을 더 개선할 수 있어요. 일반적인 아이디어는 절대 값 자체 대신 값 사이의 변화를 저장하는 것으로, 천천히 변하는 데이터를 다룰 때 필요한 공간이 훨씬 적어져요:

ALTER TABLE wikistat
MODIFY COLUMN `time` CODEC(Delta, ZSTD);

time 컬럼에 Delta 코덱을 사용했는데, 이는 시계열 데이터에 잘 맞아요. 올바른 정렬 키도 디스크 공간을 절약할 수 있어요. 보통 path로 필터링하기 때문에 정렬 키에 path를 추가할 거예요. 이는 테이블 재생성이 필요해요. 아래에서 초기 테이블과 최적화된 테이블의 CREATE 명령을 볼 수 있어요:

CREATE TABLE wikistat
(
    `time` DateTime,
    `project` String,
    `subproject` String,
    `path` String,
    `hits` UInt64
)
ENGINE = MergeTree
ORDER BY (time);
CREATE TABLE optimized_wikistat
(
    `time` DateTime CODEC(Delta(4), ZSTD(1)),
    `project` LowCardinality(String),
    `subproject` LowCardinality(String),
    `path` String,
    `hits` UInt32
)
ENGINE = MergeTree
ORDER BY (path, time);

각 테이블의 데이터가 차지하는 공간을 살펴볼게요:

SELECT
    table,
    formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed,
    formatReadableSize(sum(data_compressed_bytes)) AS compressed,
    count() AS parts
FROM system.parts
WHERE table LIKE '%wikistat%'
GROUP BY ALL;
┌─table──────────────┬─uncompressed─┬─compressed─┬─parts─┐
│ wikistat           │ 35.28 GiB    │ 12.03 GiB  │     1 │
│ optimized_wikistat │ 30.31 GiB    │ 2.84 GiB   │     1 │
└────────────────────┴──────────────┴────────────┴───────┘

최적화된 테이블은 압축 형태에서 4배 이상 적은 공간을 차지해요.

더 알아보기 (Learn more)