マージ前の重複を確かめ、FINAL・argMax・CLEANUP で扱う
한국어 원문으로 표시합니다.
목표
ReplacingMergeTree 에 변경 기록(갱신·탈퇴)을 넣고, 병합 전에는 옛 버전이 보인다는 것과 그것을 FINAL·argMax 로 가리는 법, 병합과 CLEANUP 이 실제로 무엇을 지우는지, 정렬 키와 재전송이 결과를 어떻게 바꾸는지 숫자로 확인합니다.
왜 중요한가
분석 DB 로 옮긴 운영 데이터는 계속 바뀝니다. ClickHouse 는 갱신을 "새 버전 INSERT + 나중 병합" 으로 처리하는데, 병합은 언제 일어날지 모릅니다. 그래서 같은 쿼리가 어떤 때는 맞고 어떤 때는 틀립니다. 이 실습은 SYSTEM STOP MERGES 로 "병합 전" 을 고정해 그 틀린 답을 눈으로 보고, 조회 시점에 맞는 답을 내는 두 방법을 익힙니다. 채점기는 기대값을 원본 생성 식에서 직접 계산하고, 병합이 실제로 일어났는지는 system.part_log 의 기록으로 확인합니다.
단계
- 데이터베이스
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 세 번 = 파트 세 개).- FINAL 없이 전체 행을 세는 /root/ch/replacing/q_raw.sql 과
FROM rmt.users FINAL로 세는 /root/ch/replacing/q_final.sql 을 만들고, 두 결과를 /root/ch/replacing/counts.json 에raw·final로 적으세요. - FINAL 을 쓰지 않고 요금제(plan)별 현재 사용자 수를 내는 /root/ch/replacing/q_plan.sql 을 만드세요 — 결과는 plan 과 사용자 수 두 열입니다.
rmt.merged를rmt.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 에 적으세요.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 에 적으세요.- 같은 열·엔진이지만 정렬 키가
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 에 적으세요. - 늦은 재전송을 흉내 냅니다 —
rmt.users에서ver = 1 AND user_id <= 1000인 행을rmt.merged와rmt.cleaned에 각각 다시 넣고, 두 표를 FINAL 로 센 값과 그 차이를 /root/ch/replacing/replay.json 에merged_final·cleaned_final·resurrected로 적으세요.
참고
- 서버는 파드가 뜰 때 이미 떠 있습니다.
clickhouse-client만 치면 붙습니다. 멈췄다면ch-up(서버를 다시 띄우면 STOP MERGES 가 풀립니다 — 2단계부터 다시). SYSTEM STOP MERGES는 그 표에만 걸립니다. 5–7단계의 새 표는 병합이 켜져 있어 OPTIMIZE 가 됩니다. 멈춘 표에 OPTIMIZE 를 내면 "Cancelled merging parts" 로 거절됩니다.- 병합 기록은
system.part_log에 남습니다(event_type = 'MergeParts',merged_from에 합쳐진 파트 이름). 방금 일이 안 보이면SYSTEM FLUSH LOGS. - 흔한 실수: 5–7단계에서
INSERT ... SELECT * FROM rmt.users한 번으로 옮기는 것 — 한 INSERT 안의 중복은 넣는 순간 줄어들어(optimize_on_insert) 병합할 것이 없어집니다. 버전별로 세 번 넣으세요. - 숫자를 JSON 에 따옴표 없이 받으려면
--output_format_json_quote_64bit_integers 0. - 공식 문서: ReplacingMergeTree · Working with the ReplacingMergeTree engine · OPTIMIZE · system.part_log
버전과 삭제 표시가 있는 표
데이터베이스 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.sql 과 FROM rmt.users FINAL 로 세는 /root/ch/replacing/q_final.sql 을 만들고, 두 쿼리의 결과를 /root/ch/replacing/counts.json 에 raw·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.merged 를 rmt.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.cleaned 를 rmt.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.merged 와 rmt.cleaned 에 각각 다시 넣은 뒤, 두 표를 FINAL 로 센 값과 그 차이를 /root/ch/replacing/replay.json 에 merged_final·cleaned_final·resurrected(cleaned_final − merged_final)로 적으세요.
파이프라인이 옛 묶음을 다시 보내는 상황입니다. 탈퇴 표시가 남아 있는 표에서는 무엇이 옛 행을 이기는지, 탈퇴 행을 지운 표에서는 옛 행이 누구와 겨루는지 생각해 보세요. 되살아난 사람은 1–1000 번 중 탈퇴했던 사용자입니다.