데이터 관리
데이터 관리 (Managing data)
옵저버빌리티용 ClickHouse의 대규모 데이터셋을 파티션, TTL, 스토리지 티어, 스키마 변경으로 관리하는 방법을 살펴봐요.
출처: 문서
본문
옵저버빌리티용 ClickHouse 배포는 항상 관리해야 할 대규모 데이터셋을 수반해요. ClickHouse는 데이터 관리를 돕는 여러 기능을 제공해요.
ClickStack은 최적화된 기본 스키마를 제공해요 ClickStack은 로그, 트레이스, 메트릭용 기본 제공 스키마를 제공해요. 이 스키마는 최신 ClickHouse 기능(전문 및 맵 키 검색용 텍스트 인덱스, 직접 읽기 필터링용 머티어리얼라이즈드 컬럼과 ALIAS 배열, 블록 번호 행 조회)을 통합하고 로깅과 트레이스 워크로드에 강력한 기본 제공 성능을 제공하도록 벤치마킹되었어요. 여러분의 설계를 위한 참조점으로 사용하세요.
- 표준 DDL: ClickStack이 사용하는 테이블과 스키마.
- 최적화 레시피: ClickStack 성능 튜닝. 해당 페이지의 많은 권장 사항(머티어리얼라이즈드 컬럼, 건너뛰기 인덱스, 기본 키 선택, 프로젝션, 머티어리얼라이즈드 뷰)이 자체 구축 설정에 직접 적용돼요.
파티션 (Partitions)
ClickHouse의 파티셔닝은 컬럼이나 SQL 표현식에 따라 디스크에서 데이터를 논리적으로 분리할 수 있게 해줘요. 데이터를 논리적으로 분리하면 각 파티션을 독립적으로 조작할 수 있어요. 예를 들어 삭제할 수 있어요. 이를 통해 파티션(즉 부분집합)을 시간에 따라 스토리지 티어 간에 효율적으로 이동하거나 데이터를 만료시키고/클러스터에서 효율적으로 삭제할 수 있어요. 파티셔닝은 테이블이 처음 정의될 때 PARTITION BY 절로 지정돼요. 이 절은 어떤 컬럼이든 SQL 표현식을 포함할 수 있고, 그 결과가 행이 어느 파티션으로 전송될지를 정의해요. 데이터 파트는 디스크의 각 파티션과(공통 폴더 이름 프리픽스로) 논리적으로 연관되며 격리되어 조회될 수 있어요. 아래 예시에서 기본 otel_logs 스키마는 toDate(Timestamp) 표현식으로 일 단위 파티셔닝해요. 행이 ClickHouse에 삽입되면 이 표현식이 각 행에 대해 평가되어 존재하는 파티션으로 라우팅돼요(행이 하루의 첫 행이면 파티션이 생성돼요).
CREATE TABLE default.otel_logs
(
...
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
파티션에는 여러 작업을 수행할 수 있어요. 백업, 컬럼 조작, 행별 데이터 업데이트/삭제 뮤테이션, 인덱스 클리어(예: 보조 인덱스) 등이 있어요. 예시로 otel_logs 테이블이 일 단위로 파티셔닝되어 있다고 가정해요. 구조화 로그 데이터셋으로 채우면 며칠 분의 데이터가 포함될 거예요:
SELECT Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC
┌────────day─┬───────c─┐
│ 2019-01-22 │ 2333977 │
│ 2019-01-23 │ 2326694 │
│ 2019-01-26 │ 1986456 │
│ 2019-01-24 │ 1896255 │
│ 2019-01-25 │ 1821770 │
└────────────┴─────────┘
5 rows in set. Elapsed: 0.058 sec. Processed 10.37 million rows, 82.92 MB (177.96 million rows/s., 1.42 GB/s.)
Peak memory usage: 4.41 MiB.
현재 파티션은 간단한 시스템 테이블 쿼리로 찾을 수 있어요:
SELECT DISTINCT partition
FROM system.parts
WHERE `table` = 'otel_logs'
┌─partition──┐
│ 2019-01-22 │
│ 2019-01-23 │
│ 2019-01-24 │
│ 2019-01-25 │
│ 2019-01-26 │
└────────────┘
5 rows in set. Elapsed: 0.005 sec.
이전 데이터를 저장하는 otel_logs_archive라는 다른 테이블이 있을 수 있어요. 데이터는 파티션 단위로 이 테이블로 효율적으로 이동할 수 있어요(이것은 메타데이터 변경일 뿐이에요).
CREATE TABLE otel_logs_archive AS otel_logs
--move data to archive table
ALTER TABLE otel_logs
(MOVE PARTITION tuple('2019-01-26') TO TABLE otel_logs_archive
--confirm data has been moved
SELECT
Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC
┌────────day─┬───────c─┐
│ 2019-01-22 │ 2333977 │
│ 2019-01-23 │ 2326694 │
│ 2019-01-24 │ 1896255 │
│ 2019-01-25 │ 1821770 │
└────────────┴─────────┘
4 rows in set. Elapsed: 0.051 sec. Processed 8.38 million rows, 67.03 MB (163.52 million rows/s., 1.31 GB/s.)
Peak memory usage: 4.40 MiB.
SELECT Timestamp::Date AS day,
count() AS c
FROM otel_logs_archive
GROUP BY day
ORDER BY c DESC
┌────────day─┬───────c─┐
│ 2019-01-26 │ 1986456 │
└────────────┴─────────┘
1 row in set. Elapsed: 0.024 sec. Processed 1.99 million rows, 15.89 MB (83.86 million rows/s., 670.87 MB/s.)
Peak memory usage: 4.99 MiB.
이는 INSERT INTO SELECT를 사용해 새 대상 테이블로 데이터를 다시 쓰는 것을 요구하는 다른 기법과 대조돼요.
파티션 이동 테이블 간 파티션 이동은 여러 조건을 충족해야 해요. 무엇보다 테이블이 같은 구조, 파티션 키, 기본 키, 인덱스/프로젝션을 가져야 해요. ALTER DDL에서 파티션 지정 방법에 대한 자세한 참고는 여기에 있어요.
또한 데이터는 파티션 단위로 효율적으로 삭제될 수 있어요. 이는 대체 기법(뮤테이션 또는 경량 삭제)보다 훨씬 리소스 효율적이며 선호해야 해요.
ALTER TABLE otel_logs
(DROP PARTITION tuple('2019-01-25'))
SELECT
Timestamp::Date AS day,
count() AS c
FROM otel_logs
GROUP BY day
ORDER BY c DESC
┌────────day─┬───────c─┐
│ 2019-01-22 │ 4667954 │
│ 2019-01-23 │ 4653388 │
│ 2019-01-24 │ 3792510 │
└────────────┴─────────┘
이 기능은 ttl_only_drop_parts=1 설정을 사용할 때 TTL이 활용해요. 자세한 내용은 TTL 데이터 관리를 참고하세요.
응용
위는 데이터가 파티션별로 효율적으로 이동되고 조작되는 방법을 보여줘요. 실제로 옵저버빌리티 사용 사례에서 파티션 작업을 가장 자주 활용하는 두 시나리오가 있어요:
- 티어드 아키텍처 - 스토리지 티어 간 데이터 이동(스토리지 티어 참고). 이렇게 하면 핫-콜드 아키텍처를 구축할 수 있어요.
- 효율적인 삭제 - 데이터가 지정된 TTL에 도달했을 때(TTL 데이터 관리 참고)
둘 다 아래에서 자세히 살펴볼게요.
쿼리 성능
파티션은 쿼리 성능을 도울 수 있지만, 이는 접근 패턴에 크게 의존해요. 쿼리가 몇 개의 파티션(이상적으로는 하나)만 대상으로 하면 성능이 개선될 수 있어요. 이는 보통 파티셔닝 키가 기본 키에 없고 쿼리가 그걸로 필터링할 때만 유용해요. 하지만 많은 파티션을 다뤄야 하는 쿼리는 파티셔닝을 사용하지 않을 때보다 더 나쁠 수 있어요(파트가 더 많을 수 있으므로). 파티셔닝 키가 이미 기본 키의 앞쪽 항목이라면 단일 파티션을 대상으로 하는 이점은 덜 두드러지거나 없을 거예요. 파티셔닝은 각 파티션의 값이 고유하면 GROUP BY 쿼리를 최적화하는 데도 사용할 수 있어요. 하지만 일반적으로 기본 키가 최적화되었는지 확인하고, 접근 패턴이 데이터의 특정 예측 가능한 부분집합에 접근하는 예외적인 경우에만 파티셔닝을 쿼리 최적화 기법으로 고려해야 해요. 예를 들어 일 단위 파티셔닝에 대부분의 쿼리가 지난 하루를 대상으로 하는 경우처럼요. 이 동작의 예시는 여기를 참고하세요.
TTL(Time-to-live) 데이터 관리
TTL은 ClickHouse 기반 옵저버빌리티 솔루션에서 효율적인 데이터 보존과 관리에 중요한 기능이에요. 특히 방대한 데이터가 지속적으로 생성된다는 점을 감안할 때요. ClickHouse에서 TTL을 구현하면 이전 데이터를 자동으로 만료시키고 삭제해서, 수동 개입 없이 스토리지를 최적으로 사용하고 성능을 유지할 수 있어요. 이 기능은 데이터베이스를 간결하게 유지하고 스토리지 비용을 줄이며, 가장 관련성 높고 최신 데이터에 초점을 맞춰 쿼리가 빠르고 효율적으로 유지되도록 하는 데 필수적이에요. 또한 데이터 보존 정책 준수에 도움을 주며 데이터 수명 주기를 체계적으로 관리해서 옵저버빌리티 솔루션의 전반적인 지속 가능성과 확장성을 향상시켜요. TTL은 ClickHouse에서 테이블 또는 컬럼 수준으로 지정할 수 있어요.
테이블 수준 TTL
로그와 트레이스의 기본 스키마는 지정된 기간 후 데이터를 만료시키는 TTL을 포함해요. 이는 ClickHouse exporter에서 ttl 키로 지정돼요. 예를 들어:
exporters:
clickhouse:
endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1
ttl: 72h
이 구문은 현재 Golang Duration 구문을 지원해요. h를 사용하고 파티셔닝 기간과 일치하도록 하는 것을 권장해요. 예를 들어 일 단위로 파티셔닝하면 24h, 48h, 72h처럼 일의 배수로 유지해 주세요. 그러면 ttl: 96h 같은 경우 테이블에 TTL 절이 자동으로 추가돼요.
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(4)
SETTINGS ttl_only_drop_parts = 1
기본적으로 만료된 TTL의 데이터는 ClickHouse가 데이터 파트를 병합할 때 제거돼요. ClickHouse가 데이터가 만료되었음을 감지하면 오프 스케줄 병합을 수행해요.
예약 TTL TTL은 즉시 적용되지 않고 위에 언급한 대로 예약에 따라 적용돼요. MergeTree 테이블 설정 merge_with_ttl_timeout은 삭제 TTL로 병합을 반복하기 전 최소 지연 시간(초)을 설정해요. 기본값은 14400초(4시간)예요. 하지만 이것은 최소 지연일 뿐이며 TTL 병합이 트리거되기까지 더 오래 걸릴 수 있어요. 값이 너무 낮으면 많은 리소스를 소비할 수 있는 많은 오프 스케줄 병합을 수행할 거예요. TTL 만료는 ALTER TABLE my_table MATERIALIZE TTL 명령으로 강제할 수 있어요.
중요: ttl_only_drop_parts=1 설정을 사용하는 것을 권장해요(기본 스키마 적용). 이 설정이 활성화되면 ClickHouse는 파트의 모든 행이 만료되었을 때 전체 파트를 드롭해요. 부분 파트만 드롭하는 대신 전체 파트를 드롭하는 것(ttl_only_drop_parts=0일 때 리소스 집약적 뮤테이션으로 달성)은 merge_with_ttl_timeout을 더 짧게 유지하고 시스템 성능에 미치는 영향을 줄일 수 있게 해줘요. 데이터가 TTL 만료를 수행하는 단위(예: 일)와 동일하게 파티셔닝된다면 파트는 자연스럽게 정의된 기간의 데이터만 포함할 거예요. 이렇게 하면 ttl_only_drop_parts=1이 효율적으로 적용될 수 있어요.
컬럼 수준 TTL
위 예시는 테이블 수준에서 데이터를 만료시켜요. 데이터를 컬럼 수준에서도 만료시킬 수 있어요. 데이터가 오래되면서, 조사에서 그 값이 보존에 드는 리소스 오버헤드를 정당화하지 못하는 컬럼을 드롭하는 데 사용할 수 있어요. 예를 들어 삽입 시점에 추출되지 않은 새 동적 메타데이터(예: 새 Kubernetes 라벨)가 추가되는 경우를 대비해 Body 컬럼을 보존할 것을 권장해요. 일정 기간(예: 1개월) 후 이 추가 메타데이터가 유용하지 않다는 것이 명백해질 수 있어요. 그러면 Body 컬럼 보존의 가치가 제한돼요. 아래에서는 Body 컬럼이 30일 후 드롭되는 방법을 보여줄게요.
CREATE TABLE otel_logs_v2
(
`Body` String TTL Timestamp + INTERVAL 30 DAY,
`Timestamp` DateTime,
...
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
컬럼 수준 TTL을 지정하려면 사용자가 자체 스키마를 지정해야 해요. 이는 OTel collector에서 지정할 수 없어요.
데이터 재압축
옵저버빌리티 데이터셋에는 일반적으로 ZSTD(1)을 권장하지만, 다른 압축 알고리즘이나 더 높은 압축 수준(예: ZSTD(3))을 실험할 수 있어요. 스키마 생성 시 지정할 수 있을 뿐 아니라, 압축이 설정된 기간 후 변경되도록 구성할 수도 있어요. 이것은 코덱이나 압축 알고리즘이 압축을 개선하지만 더 나쁜 쿼리 성능을 유발하는 경우에 적합할 수 있어요. 이 트레이드오프는 자주 조회되지 않는 이전 데이터에는 수용 가능할 수 있지만, 조사에서 더 자주 사용되는 최근 데이터에는 적합하지 않아요. 이 예시는 아래와 같으며, 4일 후 데이터를 삭제하는 대신 ZSTD(3)으로 압축해요.
CREATE TABLE default.otel_logs_v2
(
`Body` String,
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`Status` UInt16,
`RequestProtocol` LowCardinality(String),
`RunTime` UInt32,
`Size` UInt32,
`UserAgent` String,
`Referer` String,
`RemoteUser` String,
`RequestType` LowCardinality(String),
`RequestPath` String,
`RemoteAddress` IPv4,
`RefererDomain` String,
`RequestPage` String,
`SeverityText` LowCardinality(String),
`SeverityNumber` UInt8,
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
TTL Timestamp + INTERVAL 4 DAY RECOMPRESS CODEC(ZSTD(3))
성능 평가 다른 압축 수준과 알고리즘의 삽입 및 쿼리 성능 영향을 항상 평가할 것을 권장해요. 예를 들어 delta 코덱은 타임스탬프 압축에 도움이 될 수 있어요. 하지만 이것이 기본 키의 일부라면 필터링 성능이 저하될 수 있어요.
TTL 구성에 대한 자세한 내용과 예시는 여기에서 찾을 수 있어요. 테이블과 컬럼에 TTL을 추가하고 수정하는 방법 같은 예시는 여기에서 찾을 수 있어요. TTL이 핫-웜 아키텍처 같은 스토리지 계층을 가능하게 하는 방법은 스토리지 티어를 참고하세요.
스토리지 티어
ClickHouse에서는 서로 다른 디스크에 스토리지 티어를 만들 수 있어요. 예를 들어 핫/최신 데이터는 SSD, 이전 데이터는 S3 백엔드로요. 이 아키텍처는 조사에서 드물게 사용되어 더 낮은 쿼리 SLA가 있는 이전 데이터에 덜 비싼 스토리지를 사용할 수 있게 해줘요.
ClickHouse Cloud에는 해당 없음 ClickHouse Cloud는 S3에 백업되는 단일 데이터 사본을 사용하며 SSD 백엔드 노드 캐시를 사용해요. 따라서 ClickHouse Cloud에서는 스토리지 티어가 필요하지 않아요.
스토리지 티어 생성은 사용자가 디스크를 만들어야 해요. 그 디스크는 테이블 생성 시 지정할 수 있는 볼륨이 있는 스토리지 정책을 구성하는 데 사용돼요. 데이터는 채움 비율, 파트 크기, 볼륨 우선순위에 따라 디스크 간에 자동으로 이동할 수 있어요. 자세한 내용은 여기에서 찾을 수 있어요. ALTER TABLE MOVE PARTITION 명령으로 디스크 간 데이터를 수동으로 이동할 수 있지만, 볼륨 간 데이터 이동은 TTL로도 제어할 수 있어요. 전체 예시는 여기에서 찾을 수 있어요.
스키마 변경 관리
로그와 트레이스 스키마는 시스템 수명 동안 필연적으로 변경돼요. 예를 들어 다른 메타데이터나 pod 라벨이 있는 새 시스템을 모니터링하게 되면서요. OTel 스키마로 데이터를 생성하고 원본 이벤트 데이터를 구조화된 포맷으로 포착하면 ClickHouse 스키마는 이런 변경에 강건해져요. 하지만 새 메타데이터가 나오고 쿼리 접근 패턴이 바뀌면서, 이런 발전을 반영하도록 스키마를 업데이트하고 싶을 거예요. 스키마 변경 중 다운타임을 피하기 위해 사용자는 몇 가지 옵션이 있어요. 아래에 제시할게요.
기본 값 사용
DEFAULT 값으로 컬럼을 스키마에 추가할 수 있어요. 지정된 기본값은 INSERT 중에 지정되지 않으면 사용돼요. 스키마 변경은 머티어리얼라이즈드 뷰 변환 로직이나 이 새 컬럼을 보내도록 하는 OTel collector 구성 수정보다 먼저 수행할 수 있어요. 스키마가 변경되면 OTel collector를 재구성할 수 있어요. "SQL로 구조 추출"에서 권장하는 프로세스(OTel collector가 Null 테이블 엔진으로 데이터를 보내고, 머티어리얼라이즈드 뷰가 대상 스키마를 추출하고 결과를 저장용 대상 테이블로 보내는 방식)를 사용한다고 가정하면, 뷰는 ALTER TABLE ... MODIFY QUERY 구문으로 수정할 수 있어요. OTel 구조화 로그에서 대상 스키마를 추출하는 해당 머티어리얼라이즈드 뷰("SQL로 구조 추출"에서 사용한 것과 유사)가 있는 대상 테이블이 아래에 있다고 가정해요:
CREATE TABLE default.otel_logs_v2
(
`Body` String,
`Timestamp` DateTime,
`ServiceName` LowCardinality(String),
`Status` UInt16,
`RequestProtocol` LowCardinality(String),
`RunTime` UInt32,
`UserAgent` String,
`Referer` String,
`RemoteUser` String,
`RequestType` LowCardinality(String),
`RequestPath` String,
`RemoteAddress` IPv4,
`RefererDomain` String,
`RequestPage` String,
`SeverityText` LowCardinality(String),
`SeverityNumber` UInt8
)
ENGINE = MergeTree
ORDER BY (ServiceName, Timestamp)
CREATE MATERIALIZED VIEW otel_logs_mv TO otel_logs_v2 AS
SELECT
Body,
Timestamp::DateTime AS Timestamp,
ServiceName,
LogAttributes['status']::UInt16 AS Status,
LogAttributes['request_protocol'] AS RequestProtocol,
LogAttributes['run_time'] AS RunTime,
LogAttributes['user_agent'] AS UserAgent,
LogAttributes['referer'] AS Referer,
LogAttributes['remote_user'] AS RemoteUser,
LogAttributes['request_type'] AS RequestType,
LogAttributes['request_path'] AS RequestPath,
LogAttributes['remote_addr'] AS RemoteAddress,
domain(LogAttributes['referer']) AS RefererDomain,
path(LogAttributes['request_path']) AS RequestPage,
multiIf(Status::UInt64 > 500, 'CRITICAL', Status::UInt64 > 400, 'ERROR', Status::UInt64 > 300, 'WARNING', 'INFO') AS SeverityText,
multiIf(Status::UInt64 > 500, 20, Status::UInt64 > 400, 17, Status::UInt64 > 300, 13, 9) AS SeverityNumber
FROM otel_logs
LogAttributes에서 새 컬럼 Size를 추출하고 싶다고 가정해요. ALTER TABLE로 기본 값을 지정해 스키마에 추가할 수 있어요:
ALTER TABLE otel_logs_v2
(ADD COLUMN `Size` UInt64 DEFAULT JSONExtractUInt(Body, 'size'))
위 예시에서 기본 값을 LogAttributes의 size 키로 지정해요(존재하지 않으면 0이 될 거예요). 이는 값이 삽입되지 않은 행에 이 컬럼에 접근하는 쿼리가 Map에 접근해야 하므로 더 느리다는 뜻이에요. 이를 상수(예: 0)로 지정할 수도 있어, 값이 없는 행에 대한 후속 쿼리 비용을 줄일 수 있어요. 이 테이블을 조회하면 값이 Map에서 예상대로 채워지는 게 보여요:
SELECT Size
FROM otel_logs_v2
LIMIT 5
┌──Size─┐
│ 30577 │
│ 5667 │
│ 5379 │
│ 1696 │
│ 41483 │
└───────┘
5 rows in set. Elapsed: 0.012 sec.
이 값이 모든 향후 데이터에 삽입되도록 하려면 아래처럼 ALTER TABLE 구문으로 머티어리얼라이즈드 뷰를 수정할 수 있어요:
ALTER TABLE otel_logs_mv
MODIFY QUERY
SELECT
Body,
Timestamp::DateTime AS Timestamp,
ServiceName,
LogAttributes['status']::UInt16 AS Status,
LogAttributes['request_protocol'] AS RequestProtocol,
LogAttributes['run_time'] AS RunTime,
LogAttributes['size'] AS Size,
LogAttributes['user_agent'] AS UserAgent,
LogAttributes['referer'] AS Referer,
LogAttributes['remote_user'] AS RemoteUser,
LogAttributes['request_type'] AS RequestType,
LogAttributes['request_path'] AS RequestPath,
LogAttributes['remote_addr'] AS RemoteAddress,
domain(LogAttributes['referer']) AS RefererDomain,
path(LogAttributes['request_path']) AS RequestPage,
multiIf(Status::UInt64 > 500, 'CRITICAL', Status::UInt64 > 400, 'ERROR', Status::UInt64 > 300, 'WARNING', 'INFO') AS SeverityText,
multiIf(Status::UInt64 > 500, 20, Status::UInt64 > 400, 17, Status::UInt64 > 300, 13, 9) AS SeverityNumber
FROM otel_logs
이후 행은 삽입 시점에 Size 컬럼이 채워질 거예요.
새 테이블 만들기
위 프로세스의 대안으로, 새 스키마로 새 대상 테이블을 간단히 만들 수 있어요. 그런 다음 위 ALTER TABLE MODIFY QUERY로 머티어리얼라이즈드 뷰가 새 테이블을 사용하도록 수정할 수 있어요. 이 접근 방식으로 테이블을 버전화할 수 있어요. 예를 들어 otel_logs_v3. 이 접근 방식은 사용자에게 조회할 여러 테이블을 남겨요. 테이블 전체를 조회하려면 테이블 이름에 와일드카드 패턴을 허용하는 merge 함수를 사용할 수 있어요. 아래에서 otel_logs 테이블의 v2와 v3을 조회해 보여줄게요:
SELECT Status, count() AS c
FROM merge('otel_logs_v[2|3]')
GROUP BY Status
ORDER BY c DESC
LIMIT 5
┌─Status─┬────────c─┐
│ 200 │ 38319300 │
│ 304 │ 1360912 │
│ 302 │ 799340 │
│ 404 │ 420044 │
│ 301 │ 270212 │
└────────┴──────────┘
5 rows in set. Elapsed: 0.137 sec. Processed 41.46 million rows, 82.92 MB (302.43 million rows/s., 604.85 MB/s.)
merge 함수를 피하고 여러 테이블을 결합하는 테이블을 최종 사용자에게 노출하고 싶다면 Merge 테이블 엔진을 사용할 수 있어요. 아래에서 보여줄게요:
CREATE TABLE otel_logs_merged
ENGINE = Merge('default', 'otel_logs_v[2|3]')
SELECT Status, count() AS c
FROM otel_logs_merged
GROUP BY Status
ORDER BY c DESC
LIMIT 5
┌─Status─┬────────c─┐
│ 200 │ 38319300 │
│ 304 │ 1360912 │
│ 302 │ 799340 │
│ 404 │ 420044 │
│ 301 │ 270212 │
└────────┴──────────┘
5 rows in set. Elapsed: 0.073 sec. Processed 41.46 million rows, 82.92 MB (565.43 million rows/s., 1.13 GB/s.)
새 테이블이 추가될 때마다 EXCHANGE 테이블 구문으로 이를 업데이트할 수 있어요. 예를 들어 v4 테이블을 추가하려면 새 테이블을 만들고 이전 버전과 원자적으로 교환하면 돼요.
CREATE TABLE otel_logs_merged_temp
ENGINE = Merge('default', 'otel_logs_v[2|3|4]')
EXCHANGE TABLE otel_logs_merged_temp AND otel_logs_merged
SELECT Status, count() AS c
FROM otel_logs_merged
GROUP BY Status
ORDER BY c DESC
LIMIT 5
┌─Status─┬────────c─┐
│ 200 │ 39259996 │
│ 304 │ 1378564 │
│ 302 │ 820118 │
│ 404 │ 429220 │
│ 301 │ 276960 │
└────────┴──────────┘
5 rows in set. Elapsed: 0.068 sec. Processed 42.46 million rows, 84.92 MB (620.45 million rows/s., 1.24 GB/s.)