LabHub
배우기 러닝패스 코스

ClickHouse — 列指向分析 DB を中身から

パーツを書き直す更新、隠すだけの削除、マージ時に効く寿命

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

불변 파트 위에서 UPDATE·DELETE·TTL 이 실제로 무엇을 하는지 system.mutations·system.part_log·system.parts 로 확인하고, 멈춘 뮤테이션을 찾아 끝낸다.

왜 중요한가

ClickHouse 에서 UPDATE 한 줄은 파트 전체를 다시 쓰는 일이고, 경량 DELETE 는 행을 가릴 뿐이며, TTL 은 병합이 올 때까지 기다립니다. 이 차이를 모르면 작은 정정이 디스크 I/O 폭주가 되고, 지웠다고 믿은 행이 디스크에 남고, 실패한 뮤테이션 하나가 표의 모든 수정을 막습니다. 이 실습의 채점기는 여러분이 적은 숫자를 믿지 않습니다 — 원본 생성 식으로 남아 있어야 할 행을 다시 계산하고, 가림막을 끈 채(apply_deleted_mask = 0)로도 세고, system.part_log 의 기록과 대조합니다.

단계

  1. 데이터베이스 mut 와 표 mut.events 를 만드세요 — 열 event_date Date, ts DateTime, user_id UInt32, email String, amount UInt32, status LowCardinality(String), legal_hold UInt8 (이 순서), MergeTree, ORDER BY (user_id, ts). /opt/lab/fixtures/mutation/events.sql 로 40만 행을 한 번 넣고 OPTIMIZE TABLE mut.events FINAL 로 파트를 하나로 만드세요.
  2. ALTER TABLE mut.events UPDATE status = 'refunded' WHERE status = 'refund_req'mutations_sync = 2 로 실행하고, 그 mutation_id, 전후의 활성 파트 이름(part_before·part_after), system.part_log 의 그 MutatePart 가 쓴 행 수(rows_rewritten)를 /root/ch/mutation/mutation.json 에 적으세요.
  3. user_id = 777 의 행을 ALTER TABLE ... DELETE 로 지우세요(mutations_sync = 2).
  4. user_id = 888 의 행을 경량 DELETE(DELETE FROM)로 지우세요. 곧바로 보통 SELECT 로 센 그 사용자의 행(visible_rows), SETTINGS apply_deleted_mask = 0 으로 센 행(masked_rows), system.parts 의 활성 파트 rows(part_rows)를 /root/ch/mutation/lwd.json 에 적은 뒤 OPTIMIZE TABLE mut.events FINAL 을 실행하세요.
  5. email 열에 TTL if(legal_hold = 1, toDate('2100-01-01'), event_date + INTERVAL 90 DAY) 를 붙이고(MODIFY COLUMN email String TTL ...), 기존 파트에 적용하세요.
  6. 표 TTL event_date + INTERVAL 180 DAY DELETE WHERE status = 'test' 를 붙이고(MODIFY TTL), 기존 파트에 적용하세요.
  7. mut.hits (ts DateTime, user_id UInt32, hits UInt32, max_hits UInt32 DEFAULT hits, sum_hits UInt64 DEFAULT hits)MergeTree, PRIMARY KEY (user_id, toStartOfDay(ts), ts), TTL ts + INTERVAL 30 DAY GROUP BY user_id, toStartOfDay(ts) SET max_hits = max(max_hits), sum_hits = sum(sum_hits) 로 만들고, /opt/lab/fixtures/mutation/hits.sql한 번 넣은 뒤 OPTIMIZE TABLE mut.hits FINAL 로 요약을 적용하세요.
  8. ALTER TABLE mut.events UPDATE amount = toUInt32(email) WHERE legal_hold = 1 을 (기다리지 않고) 제출하고, 실패가 기록되면 그 뮤테이션의 mutation_id·is_done·parts_to_do·latest_fail_error_code_name(열쇠 error_code_name)을 /root/ch/mutation/stuck.json 에 적은 뒤 KILL MUTATION 으로 끝내세요.

참고

파트 하나짜리 이벤트 표

데이터베이스 mut 와 표 mut.events 를 만드세요. 열 event_date Date, ts DateTime, user_id UInt32, email String, amount UInt32, status LowCardinality(String), legal_hold UInt8 순서, MergeTree, ORDER BY (user_id, ts). /opt/lab/fixtures/mutation/events.sql 로 40만 행을 한 번 넣고 OPTIMIZE TABLE mut.events FINAL 로 파트를 하나로 만드세요.

파트를 하나로 만들어 두면 뒤의 뮤테이션마다 파트 이름이 어떻게 바뀌는지 한 줄로 따라갈 수 있습니다. 자료는 전부 2024년이라, 뒤에서 붙일 TTL 이 언제 채점해도 같은 행을 지웁니다.

2% 를 바꾸려고 파트 전체를 쓴다

ALTER TABLE mut.events UPDATE status = 'refunded' WHERE status = 'refund_req' SETTINGS mutations_sync = 2 를 실행하세요. system.mutations 의 그 mutation_id, 실행 전후 활성 파트 이름 part_before·part_after, system.part_log 에서 part_after 를 만든 MutatePart 기록의 rowsrows_rewritten 으로 /root/ch/mutation/mutation.json 에 적으세요.

파트는 고칠 수 없으므로 뮤테이션은 새 파트를 통째로 씁니다. 새 파트 이름 끝에 붙은 숫자가 뮤테이션 번호입니다. part_log 는 1초마다 비워지니 SYSTEM FLUSH LOGS 뒤에 보고, merged_from 이 원래 파트를 가리키는지 확인하세요. 바뀐 행 수와 다시 쓴 행 수를 비교해 보세요.

무거운 삭제 — ALTER TABLE ... DELETE

user_id = 777 의 행을 ALTER TABLE mut.events DELETE WHERE user_id = 777 SETTINGS mutations_sync = 2 로 지우세요.

ALTER DELETE 도 뮤테이션입니다. 해당 행이 든 파트를 행을 뺀 채로 다시 씁니다. 끝나면 가림막을 끈 조회(SETTINGS apply_deleted_mask = 0)로도 그 행이 보이지 않아야 합니다.

경량 DELETE 는 가릴 뿐이다

DELETE FROM mut.events WHERE user_id = 888 을 실행하고, 곧바로 보통 SELECT 로 센 그 사용자의 행을 visible_rows, SETTINGS apply_deleted_mask = 0 으로 센 행을 masked_rows, system.parts 의 활성 파트 rowspart_rows/root/ch/mutation/lwd.json 에 적으세요. 그다음 OPTIMIZE TABLE mut.events FINAL 을 실행하세요.

경량 DELETE 는 숨은 열 _row_exists 에 0 을 쓰는 뮤테이션으로 바뀝니다(system.mutations 의 command 를 보세요). SELECT 는 그 표시를 보고 행을 가리지만 파트에는 행이 그대로 있어 rows 가 줄지 않습니다. 실제로 빠지는 것은 병합 때입니다.

열 TTL — 기한 지난 값만 지운다

ALTER TABLE mut.events MODIFY COLUMN email String TTL if(legal_hold = 1, toDate('2100-01-01'), event_date + INTERVAL 90 DAY) 를 실행하고, 기존 파트에도 적용하세요(MATERIALIZE TTL, mutations_sync = 2). 법적 보존(legal_hold = 1) 행의 email 은 남아야 합니다.

열 TTL 은 기한이 지난 값을 그 타입의 기본값(String 이면 빈 문자열)으로 바꿉니다. 보존 행에는 먼 미래 날짜를 돌려주는 식으로 예외를 만듭니다. TTL 을 바꾸면 기본으로 MATERIALIZE TTL 뮤테이션이 함께 생기는지 system.mutations 에서 보세요.

행 TTL — 조건에 맞는 행만 지운다

ALTER TABLE mut.events MODIFY TTL event_date + INTERVAL 180 DAY DELETE WHERE status = 'test' 를 실행하고, 기존 파트에도 적용하세요. test 가 아닌 행은 하나도 사라지면 안 됩니다.

자료는 전부 2024년이라 180일이 지났습니다. WHERE 가 없으면 모든 행이 기한을 넘겨 표가 통째로 빕니다. TTL 은 병합 때 적용되므로 MATERIALIZE TTL 이나 OPTIMIZE FINAL 로 지금 적용합니다.

GROUP BY TTL — 오래된 행을 요약으로

mut.hits (ts DateTime, user_id UInt32, hits UInt32, max_hits UInt32 DEFAULT hits, sum_hits UInt64 DEFAULT hits)MergeTree, PRIMARY KEY (user_id, toStartOfDay(ts), ts), TTL ts + INTERVAL 30 DAY GROUP BY user_id, toStartOfDay(ts) SET max_hits = max(max_hits), sum_hits = sum(sum_hits) 로 만들고, /opt/lab/fixtures/mutation/hits.sql한 번 넣은 뒤 OPTIMIZE TABLE mut.hits FINAL 을 실행하세요.

GROUP BY 에 쓰는 열은 기본 키의 앞부분이어야 해서 PRIMARY KEY 에 toStartOfDay(ts) 를 넣었습니다. max_hits·sum_hits 의 기본값을 hits 로 둬야 요약 전 한 행이 자기 값으로 시작합니다. INSERT 직후와 OPTIMIZE 뒤의 행 수를 비교해 보세요.

멈춘 뮤테이션을 찾아 끝낸다

ALTER TABLE mut.events UPDATE amount = toUInt32(email) WHERE legal_hold = 1 을 (mutations_sync 없이) 제출하세요. system.mutations 에 실패가 기록되면 그 뮤테이션의 mutation_id·is_done·parts_to_do·latest_fail_error_code_name(열쇠 이름 error_code_name)을 /root/ch/mutation/stuck.json 에 적고, KILL MUTATION 으로 끝내세요.

이메일 문자열은 숫자로 읽을 수 없어 뮤테이션이 파트마다 실패하고 계속 다시 시도합니다. 되돌리기(롤백)는 없고, 뒤에 오는 뮤테이션은 전부 이것에 막힙니다. latest_fail_reason 이 채워질 때까지 몇 초 기다린 뒤 기록하고, KILL MUTATION WHERE database = 'mut' AND mutation_id = '…' 로 끝냅니다.