LabHub
배우기 러닝패스 코스

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

マージ前の重複を確かめ、FINAL・argMax・CLEANUP で扱う

LabHub 에서 이어서 보기

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

목표

ReplacingMergeTree 에 변경 기록(갱신·탈퇴)을 넣고, 병합 전에는 옛 버전이 보인다는 것과 그것을 FINAL·argMax 로 가리는 법, 병합과 CLEANUP 이 실제로 무엇을 지우는지, 정렬 키와 재전송이 결과를 어떻게 바꾸는지 숫자로 확인합니다.

왜 중요한가

분석 DB 로 옮긴 운영 데이터는 계속 바뀝니다. ClickHouse 는 갱신을 "새 버전 INSERT + 나중 병합" 으로 처리하는데, 병합은 언제 일어날지 모릅니다. 그래서 같은 쿼리가 어떤 때는 맞고 어떤 때는 틀립니다. 이 실습은 SYSTEM STOP MERGES 로 "병합 전" 을 고정해 그 틀린 답을 눈으로 보고, 조회 시점에 맞는 답을 내는 두 방법을 익힙니다. 채점기는 기대값을 원본 생성 식에서 직접 계산하고, 병합이 실제로 일어났는지는 system.part_log 의 기록으로 확인합니다.

단계

  1. 데이터베이스 rmt 와 표 rmt.users 를 만드세요 — 열 user_id UInt64, email String, plan LowCardinality(String), score UInt32, ver UInt32, is_deleted UInt8(이 순서), 엔진 ReplacingMergeTree(ver, is_deleted), ORDER BY user_id.
  2. SYSTEM STOP MERGES rmt.users 로 이 표의 병합을 멈춘 뒤 /opt/lab/fixtures/replacing/changes.sql한 번 실행하세요(INSERT 세 번 = 파트 세 개).
  3. FINAL 없이 전체 행을 세는 /root/ch/replacing/q_raw.sqlFROM rmt.users FINAL 로 세는 /root/ch/replacing/q_final.sql 을 만들고, 두 결과를 /root/ch/replacing/counts.jsonraw·final 로 적으세요.
  4. FINAL 을 쓰지 않고 요금제(plan)별 현재 사용자 수를 내는 /root/ch/replacing/q_plan.sql 을 만드세요 — 결과는 plan 과 사용자 수 두 열입니다.
  5. rmt.mergedrmt.users 와 같은 열·엔진으로 만들어 rmt.users 의 행을 ver = 1, ver = 2, ver = 3 순으로 INSERT 세 번에 옮기고 OPTIMIZE TABLE rmt.merged FINAL 을 실행한 뒤, rows_after_merge(count())·deleted_rows_kept(is_deleted = 1 인 행 수)·final_count(FINAL 로 센 수)를 /root/ch/replacing/merged.json 에 적으세요.
  6. rmt.cleaned 를 같은 열·엔진에 표 설정 allow_experimental_replacing_merge_with_cleanup = 1 을 붙여 만들고, 같은 방법으로 세 번 넣은 뒤 OPTIMIZE TABLE rmt.cleaned FINAL CLEANUP 을 실행해 rows_after_cleanup·deleted_rows_left/root/ch/replacing/cleanup.json 에 적으세요.
  7. 같은 열·엔진이지만 정렬 키가 ORDER BY (user_id, plan)rmt.bad 를 만들어 같은 방법으로 세 번 넣고 OPTIMIZE TABLE rmt.bad FINAL 한 뒤, good_final(rmt.merged 를 FINAL 로 센 수)·bad_final(rmt.bad 를 FINAL 로 센 수)·bad_users(rmt.bad FINAL 의 서로 다른 user_id 수)를 /root/ch/replacing/key.json 에 적으세요.
  8. 늦은 재전송을 흉내 냅니다 — rmt.users 에서 ver = 1 AND user_id <= 1000 인 행을 rmt.mergedrmt.cleaned 에 각각 다시 넣고, 두 표를 FINAL 로 센 값과 그 차이를 /root/ch/replacing/replay.jsonmerged_final·cleaned_final·resurrected 로 적으세요.

참고

버전과 삭제 표시가 있는 표

데이터베이스 rmt 와 표 rmt.users 를 만드세요. 열은 user_id UInt64, email String, plan LowCardinality(String), score UInt32, ver UInt32, is_deleted UInt8 순서, 엔진은 ReplacingMergeTree(ver, is_deleted), 정렬 키는 ORDER BY user_id 입니다.

엔진의 첫 인자는 어느 행이 이기는지 정하는 버전 열, 둘째 인자는 이긴 행이 삭제인지 알려 주는 열입니다. 삭제 표시 열은 버전 열 없이는 쓸 수 없습니다. 정렬 키가 곧 "같은 행" 의 정의이므로 바뀌지 않는 식별자만 둡니다.

병합을 멈추고 변경 기록 세 묶음을 넣는다

SYSTEM STOP MERGES rmt.users 로 이 표의 병합을 멈춘 뒤 /opt/lab/fixtures/replacing/changes.sql한 번 실행하세요. 처음 적재·갱신·탈퇴 세 INSERT 가 파트 세 개로 남아야 합니다.

병합은 백그라운드에서 아무 때나 일어나서, 가만두면 "병합 전" 상태가 몇 초 만에 사라질 수 있습니다. 멈추는 것을 넣기 전에 해야 합니다. 이미 넣었다면 멈춘 뒤 TRUNCATE 하고 다시 넣으세요.

FINAL 없이 세기와 FINAL 로 세기

FINAL 없이 rmt.users 의 전체 행을 세는 /root/ch/replacing/q_raw.sqlFROM rmt.users FINAL 로 세는 /root/ch/replacing/q_final.sql 을 만들고, 두 쿼리의 결과를 /root/ch/replacing/counts.jsonraw·final 로 적으세요.

FINAL 은 조회하면서 병합 규칙(정렬 키가 같으면 ver 가 큰 행만, 그 행이 삭제 표시면 빼기)을 적용합니다. 두 숫자의 차이는 옛 버전 행과 탈퇴 행을 합친 만큼입니다. q_final.sql 에 WHERE 를 붙이지 마세요.

FINAL 없이 같은 답 — argMax

FINAL 을 쓰지 않고 요금제(plan)별 현재 사용자 수를 내는 /root/ch/replacing/q_plan.sql 을 만드세요. 결과는 plan 과 사용자 수 두 열이며, 탈퇴한 사용자는 빠져야 합니다.

그냥 GROUP BY plan 으로 세면 옛 버전과 탈퇴 행까지 셉니다. 먼저 사용자마다 한 행으로 줄여야 합니다 — user_id 로 묶고 argMax(값, ver) 로 버전이 가장 큰 행의 plan 과 is_deleted 를 고른 뒤, 바깥에서 탈퇴자를 빼고 plan 으로 다시 묶습니다.

병합해도 탈퇴 행은 남는다

rmt.mergedrmt.users 와 같은 열·엔진으로 만들어 rmt.users 의 행을 ver = 1·ver = 2·ver = 3 순으로 INSERT 세 번에 옮기고 OPTIMIZE TABLE rmt.merged FINAL 을 실행한 뒤, rows_after_merge(count())·deleted_rows_kept(is_deleted = 1 인 행 수)·final_count(FINAL 로 센 수)를 /root/ch/replacing/merged.json 에 적으세요.

CREATE TABLE ... AS 다른표 는 열과 엔진을 복사하지만 STOP MERGES 상태는 복사하지 않습니다. 병합 뒤 행 수가 사용자 수와 같아지는지, 그런데도 FINAL 없이 센 값이 왜 여전히 틀리는지 보세요. 병합이 실제로 일어났는지는 system.part_log 에 남습니다.

CLEANUP 병합은 탈퇴 행까지 지운다

rmt.cleanedrmt.users 와 같은 열·엔진에 표 설정 allow_experimental_replacing_merge_with_cleanup = 1 을 붙여 만들고, 5단계와 같이 세 번 옮긴 뒤 OPTIMIZE TABLE rmt.cleaned FINAL CLEANUP 을 실행해 rows_after_cleanup(count())·deleted_rows_left(is_deleted = 1 인 행 수)를 /root/ch/replacing/cleanup.json 에 적으세요.

CREATE TABLE 새표 AS 다른표 뒤에 SETTINGS 를 붙일 수 있습니다. 설정 없이 CLEANUP 을 내면 서버가 거절합니다 — 탈퇴 행을 지우면 나중에 옛 버전이 들어왔을 때 막을 수 없게 되기 때문에 일부러 켜야 하는 기능입니다.

정렬 키가 "같은 행" 을 정한다

같은 열·엔진이지만 ORDER BY (user_id, plan)rmt.bad 를 만들어 같은 방법으로 세 번 옮기고 OPTIMIZE TABLE rmt.bad FINAL 한 뒤, good_final(rmt.merged 를 FINAL 로 센 수)·bad_final(rmt.bad 를 FINAL 로 센 수)·bad_users(rmt.bad FINAL 의 서로 다른 user_id 수)를 /root/ch/replacing/key.json 에 적으세요.

정렬 키에 plan 이 들어가면 요금제가 바뀐 사용자의 옛 행과 새 행은 키가 달라 한 무리가 되지 못합니다. 탈퇴 행도 탈퇴 직전 요금제의 무리만 가립니다. bad_final 이 사용자 수보다 큰지, bad_users 가 현재 사용자 수보다 큰지 보세요.

CLEANUP 뒤에 옛 행이 다시 오면

rmt.users 에서 ver = 1 AND user_id <= 1000 인 행을 rmt.mergedrmt.cleaned 에 각각 다시 넣은 뒤, 두 표를 FINAL 로 센 값과 그 차이를 /root/ch/replacing/replay.jsonmerged_final·cleaned_final·resurrected(cleaned_final − merged_final)로 적으세요.

파이프라인이 옛 묶음을 다시 보내는 상황입니다. 탈퇴 표시가 남아 있는 표에서는 무엇이 옛 행을 이기는지, 탈퇴 행을 지운 표에서는 옛 행이 누구와 겨루는지 생각해 보세요. 되살아난 사람은 1–1000 번 중 탈퇴했던 사용자입니다.