TT Lab
はじめる
学ぶ 学習パス コース

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

ミューテーション・軽量 DELETE・TTL — 変更できないパーツを変える方法

TT Labで続きを見る

一言でいうと

MergeTreeのパートは、一度書くと変わりません。そのため、UPDATE・DELETEはパートをまるごと新しく書き直す非同期の作業(ミューテーション)になり、軽量DELETEは削除する行を隠す印だけを書いて、実際の削除はマージに任せ、TTLはマージが起きるときに、期限が過ぎた行や値を取り除いたり、要約したりするルールです。3つとも「今すぐ、この行だけ」ではないことを知っておく必要があります。

なぜ必要なのか

前のモジュールで、パートが不変という設計が、書き込みと圧縮を速くすることを見ました。代償は、修正です。個人情報の削除依頼、誤って入ったステータス値の訂正、古いログの整理は、どの分析DBにも来ます。行指向DBのように、その場で数バイトを直すことはできないので、ClickHouseはこの要求を3つのツールに分けました。まれで重い訂正はミューテーション、頻繁な削除は軽量DELETE、ルールで表せる寿命管理はTTLです。公式ドキュメントの「Avoid mutations」が、最初のツールをできるだけ使わないよう述べている理由を、数値で見るのが、このモジュールの目標です。

どう動くのか

左に、アクティブなパートall_1_1_1があります。ALTER UPDATEはミューテーションとして登録され、変更される行が2%でも、パート全体を読んで新しいパートall_1_1_1_2を書き、古いパートは非アクティブになります。名前の末尾の2が、ミューテーション番号です。真ん中には、軽量DELETEがあります。パートのほかの列はそのままにして、隠れた列_row_existsに、削除する行を0としてマークするだけなので、SELECTはその行を隠しますが、パートのrowsは減りません。右には、マージがあります。OPTIMIZE FINALやバックグラウンドのマージが、隠された行を実際に取り除き、同じときにTTLのルールが、期限が過ぎた行を削除したり、列の値をデフォルト値にしたり、GROUP BYで1行にまとめたりします

ミューテーション。ALTER TABLE t UPDATE ... WHERE ...とALTER TABLE t DELETE WHERE ...は、ドキュメントがわざとALTERで始まるようにした、重いコマンドです。投入すると、system.mutationsに1行ができ、バックグラウンドでその行が入っているパートを新しく書き直して、アトミックに差し替えます。デフォルトは非同期で、mutations_sync = 2を指定すると、終わるまで待ちます。このPodで、40万行のパートの2%(8千行あまり)のstatusを変更したら、パート名がall_1_1_1からall_1_1_1_2に変わり、system.part_logのMutatePartの記録は、40万行を読んで、40万行を書きました。名前の末尾の2が、そのミューテーションの番号です。ドキュメントが述べるいくつかの性質として、ミューテーションは投入順に適用され、投入前に入ったデータにだけかかり、元に戻せず、サーバーを再起動しても続きから動きます。ソートキー・パーティションキーの列は、UPDATEできません。

失敗したミューテーションは、もっと恐ろしいです。文字列のメールアドレスをtoUInt32に変えるミューテーションを入れたら、system.mutationsにis_done = 0、latest_fail_error_code_name = CANNOT_PARSE_TEXTが残り、自分では止まりませんでした。そのあとにmutations_sync = 2で入れたまともなUPDATEも、前のものに阻まれて、UNFINISHEDエラーで返ってきました。終わらせる方法はKILL MUTATIONだけです。

軽量DELETE。DELETE FROM t WHERE ...は、内部的にUPDATE _row_exists = 0 WHERE ...ミューテーションになります(system.mutationsのcommandにそのまま見えます)。Wideパートなら、隠れた列_row_existsだけを書き、ほかの列ファイルはハードリンクで再利用します。以後のSELECTは、このマスクをPREWHEREで適用します。このPodで、ユーザー888の22行を削除した直後、通常のSELECTは0行、SETTINGS apply_deleted_mask = 0は22行、system.partsのrowsはそのままでした。行が実際に取り除かれたのは、OPTIMIZE ... FINALでマージしたあとでした。ドキュメントも「次のマージのときに物理的に削除される」と述べています。プロジェクションのあるテーブルでは、デフォルトで拒否されることは、前のモジュールで見ました。

TTL。TTLはテーブルや列に付けるルールで、マージのときに適用されます。マージがなければ、ドキュメントのmerge_with_ttl_timeout(デフォルトは4時間)ごとにTTLマージを試みます。使い方は3つあります。

形式 例 期限が過ぎると
列TTL email String TTL event_date + INTERVAL 90 DAY その列の値を、型のデフォルト値('')にします
行TTL+WHERE TTL event_date + INTERVAL 180 DAY DELETE WHERE status = 'test' 条件に合う行を削除します
GROUP BYによる要約 TTL ts + INTERVAL 30 DAY GROUP BY user_id, toStartOfDay(ts) SET ... 複数の行を1行にまとめます

MODIFY TTLや、TTLが付いたMODIFY COLUMNは、デフォルト(materialize_ttl_after_modify = 1)で、既存のパートに適用するMATERIALIZE TTLミューテーションを一緒に作ります。つまり、TTLを変えるのも、パートを書き直す作業です。GROUP BYによる要約のグループ化の列は、プライマリキーの先頭部分である必要があります。このPodで、2024年3月の閲覧記録20万行は、INSERT直後は20万行のままで、OPTIMIZE FINALのあと(ユーザー, 日付)の15,500行に減りました。

TTLの式がnow()基準なので、結果が時間によって変わる点も、知っておいてください。このラボはデータをすべて2024年にし、法的保存の行はif(legal_hold = 1, toDate('2100-01-01'), ...)で遠い未来を与えて、採点の時刻に関係なく、同じ行が同じ判定を受けるようにしました。

現場での姿

「GDPRの削除依頼が1日に数百件あって、1件ごとにALTER DELETEを実行する」という設計は、すぐにミューテーションの待ち行列をためます。テーブルのすべてのミューテーションは順番に動き、1つが失敗で止まれば、後ろがすべて阻まれます。削除が頻繁なら、軽量DELETEやReplacingMergeTreeの削除マーカー、パーティション単位のDROPを先に検討します。

2番目は、「軽量DELETEをしたから、ディスクが空いたはずだ」という誤解です。マスクを書いただけなので、容量はマージのあとで戻ります。法的に「いつまでに物理削除」を約束する必要があるなら、ドキュメントが勧めるように、min_age_to_force_merge_secondsやALTER DELETEを使います。

3番目は、TTLが「効かない」という報告です。TTLはマージのときにしか適用されないので、静かなテーブルでは数時間遅れることがあります。確認するときは、MATERIALIZE TTLですぐに適用し、普段は、TTLと同じ時間単位でパーティションを分けて、まるごと落ちるようにするのが、ドキュメントの推奨です。

次のラボですること

mut.eventsの40万行をパート1つにしてから、ALTER UPDATEがパートをまるごと書き直す様子をpart_logで確認します。ユーザー777はALTER DELETEで、888は軽量DELETEで削除し、マスクと物理削除の違いを数えます。email列のTTLとtest行のTTLを適用し、閲覧記録をGROUP BY TTLで要約したあと、最後に、失敗して止まったミューテーションを記録してKILLします。