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

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

月次パーティションを絞り込み、削除し、切り離し、コピーする

TT Labで続きを見る

目標

月単位のパーティションのテーブルで、プルーニング(Min-Max)が何を捨てるかをEXPLAINで読み、パーティションのないテーブルと読んだ行数を比べます。DROP/DETACH/ATTACH PARTITIONとDELETEの違いをsystem.mutations・part_logで確認し、細かく分けすぎたパーティションがINSERTを止める様子を見ます。

なぜ重要なのか

パーティションキーは、一度決めると変えにくく、誤って選ぶと(細かすぎると)パートが爆発してINSERTが止まります。逆にうまく選べば、保管期間の管理がファイル単位で終わります。このラボの採点ツールは、書き込まれた数値をそのまま信用しません。保存されたSELECTにEXPLAINを付けて読み取り専用でもう一度実行し、パーティションの操作はテーブルの行ハッシュとsystem.mutations・part_log・query_logで突き合わせます。

ステップ

  1. データベースptnとテーブルptn.salesを作成してください。列はts DateTime, region LowCardinality(String), order_id UInt64, amount UInt32(この順序)、エンジンはMergeTree、PARTITION BY toYYYYMM(ts)、ORDER BY (region, ts)です。
  2. /opt/lab/fixtures/partition/sales.sqlを1回だけ実行して150万行を入れ、OPTIMIZE TABLE ptn.sales FINALでまとめたあと、パーティションごとにpartition_id・part(パート名)・rowsを持つJSON配列を保存してください(保存先: /root/ch/partition/partitions.json)。
  3. 2026年8月(ts >= '2026-08-01 00:00:00' AND ts < '2026-09-01 00:00:00')のamountの合計を求めるクエリ(/root/ch/partition/q_aug.sql)を作成し、EXPLAIN indexes = 1の出力を保存してください(保存先: /root/ch/partition/explain_aug.txt)。Min-Max段階の残ったパートの数と全体のパートの数、およびこのクエリのrows_readを、parts_selected・parts_total・rows_readとして書き込んでください(保存先: /root/ch/partition/prune.json)。
  4. パーティションなしで同じ列・ORDER BY (region, ts)のptn.sales_flatを作成し、ptn.salesの行を移してまとめてください。同じ8月の条件でそのテーブルを読むクエリ(/root/ch/partition/q_flat.sql)を作成し、2つのクエリのrows_readをpartitioned_rows_read・flat_rows_readとして書き込んでください(保存先: /root/ch/partition/compare.json)。
  5. ptn.salesのコピーptn.s_drop・ptn.s_deleteを作成し(CREATE TABLE ... AS ptn.sales、行のコピー、マージ)、7月を削除してください。s_dropはDROP PARTITION 202607、s_deleteはALTER TABLE ... DELETE WHERE toYYYYMM(ts) = 202607 SETTINGS mutations_sync = 1を使います。2つのテーブルのsystem.mutationsの行数と、s_deleteのpart_logのMutatePartイベントの数を、drop_mutations・delete_mutations・delete_mutated_partsとして書き込んでください(保存先: /root/ch/partition/dropdel.json)。
  6. コピーptn.s_detachを同じ方法で作成し、DETACH PARTITION 202609で切り離したあと、system.detached_partsに表示されるパート名を確認し、ATTACH PARTITION 202609でもう一度付け直してください。切り離すときの名前と、付け直したあとのアクティブなパート名を、detached_part・attached_partとして書き込んでください(保存先: /root/ch/partition/detach.json)。
  7. CREATE TABLE ptn.sales_jul AS ptn.salesで空のテーブルを作成し、INSERTなしでALTER TABLE ptn.sales_jul ATTACH PARTITION 202607 FROM ptn.salesを使って、7月をコピーしてください。
  8. 同じ列でPARTITION BY (toDate(ts), region)・ORDER BY tsのptn.sales_fineを作成し、ptn.salesの全体をINSERTしてみて、出たエラーを保存してください(保存先: /root/ch/partition/toofine.txt)。ptn.salesで日付と地域の組み合わせがいくつあるかを数えて、partitions_neededとして書き込んでください(保存先: /root/ch/partition/fine.json)。

参考

月単位のパーティションのテーブルを作成する

データベースptnとテーブルptn.salesを作成してください。列はts DateTime, region LowCardinality(String), order_id UInt64, amount UInt32の順序、エンジンはMergeTree、PARTITION BY toYYYYMM(ts)、ORDER BY (region, ts)です。

PARTITION BYは、ORDER BYの前に書いても後ろに書いてもかまいません。toYYYYMM(ts)は202607のような整数を返し、この値がそのままpartition_idになります。ソートキーにパーティションキーを入れなくてもかまいません。1つのパートの中では、どのみち同じ月だからです。

3か月分を入れてパーティションごとのパートを書き込む

/opt/lab/fixtures/partition/sales.sqlを1回だけ実行して1,500,000行を入れ、OPTIMIZE TABLE ptn.sales FINALでまとめてください。パーティションごとにpartition_id・part(アクティブなパート名)・rowsを持つオブジェクトのJSON配列を保存してください(保存先: /root/ch/partition/partitions.json)。

1回のINSERTでも、パーティションが3つならパートも少なくとも3つになります。FINALはパーティションごとに別々にまとめるので、まとめたあともパートはパーティションの数だけ残ります。JSONEachRowで受け取った行をjq -s .でまとめると、配列になります。

8月だけで絞り込むと: Min-Maxプルーニング

2026年8月(ts >= '2026-08-01 00:00:00' AND ts < '2026-09-01 00:00:00')のamountの合計を求めるクエリ(/root/ch/partition/q_aug.sql)を作成し、そのSELECTにEXPLAIN indexes = 1を付けた出力を保存してください(保存先: /root/ch/partition/explain_aug.txt)。Min-Max段階のParts: 남은/전체(プレースホルダーは残ったパート数と全体のパート数です)と、このクエリのrows_read(クエリ条件キャッシュを無効にして)を、parts_selected・parts_total・rows_readとして書き込んでください(保存先: /root/ch/partition/prune.json)。

パートごとに、パーティションキーに使われた列(ts)の最小値・最大値が記録されていて、範囲が重ならないパートは開きません。Min-Maxの次のPartition段階は、残ったパートだけをもう一度見ます。読んだ行数を、8月のパーティションの行数と見比べてみてください。

パーティションを外すとどれだけ多く読むか

パーティションなしで同じ列・ORDER BY (region, ts)のptn.sales_flatを作成し、ptn.salesの行を移してOPTIMIZE ... FINALでまとめてください。同じ8月の条件でそのテーブルのamountの合計を求めるクエリ(/root/ch/partition/q_flat.sql)を作成し、q_aug.sqlとq_flat.sqlのrows_readを、partitioned_rows_read・flat_rows_readとして書き込んでください(保存先: /root/ch/partition/compare.json)。

CREATE TABLE ... AS ptn.salesはPARTITION BYまでコピーするので、列を自分で書いて作成します。パーティションがなくても、tsはソートキーの2番目の列で、前の列のregionは5種類しかありません。前のモジュールのgeneric exclusion searchが何をしてくれるのかを、思い出してみてください。

同じ7月を2つの方法で削除する

ptn.salesのコピーptn.s_drop・ptn.s_deleteを作成し(CREATE TABLE ... AS ptn.sales、行のコピー、OPTIMIZE ... FINAL)、7月を削除してください。ptn.s_dropはALTER TABLE ptn.s_drop DROP PARTITION 202607、ptn.s_deleteはALTER TABLE ptn.s_delete DELETE WHERE toYYYYMM(ts) = 202607 SETTINGS mutations_sync = 1を使います。2つのテーブルのsystem.mutationsの行数と、s_deleteのpart_logのMutatePartイベントの数を、drop_mutations・delete_mutations・delete_mutated_partsとして書き込んでください(保存先: /root/ch/partition/dropdel.json)。

DROP PARTITIONは、パートをテーブルから切り離す操作なので、行を読むことも書くこともしません。ALTER DELETEはミューテーションになり、パートを新しいバージョンに書き直します。削除する行がないパートも新しいバージョンになるのかを、part_logで数えてみてください。mutations_sync = 1は、ミューテーションが終わるまで待たせます。

切り離して付け直すと名前が変わる

コピーptn.s_detachをステップ5と同じ方法で作成し、ALTER TABLE ptn.s_detach DETACH PARTITION 202609で切り離してください。system.detached_partsに表示されるパート名を確認したあと、ATTACH PARTITION 202609でもう一度付け直し、切り離すときの名前と、付け直したあとの202609のアクティブなパート名を、detached_part・attached_partとして書き込んでください(保存先: /root/ch/partition/detach.json)。

DETACHは削除せずにdetached/ディレクトリへ移し、その間テーブルはそのパートを忘れます(count()が減ります)。付け直されたパートは、テーブルで新しいブロック番号を受け取るので、名前の真ん中の数字が変わります。切り離した直後に、名前を変数に入れておいてください。

INSERTなしで1か月分をコピーしてくる

CREATE TABLE ptn.sales_jul AS ptn.salesで構造が同じ空のテーブルを作成し、ALTER TABLE ptn.sales_jul ATTACH PARTITION 202607 FROM ptn.salesで7月のパーティションをコピーしてください。ptn.salesはそのまま150万行である必要があり、ptn.sales_julにはINSERTをしないでください。

ATTACH PARTITION ... FROMは、コピー元からもコピー先からも削除せずにパーティションをコピーします。2つのテーブルの構造とパーティションキーが同じである必要があります。採点ツールは、query_logでこのテーブルへのINSERTがなかったかも確認します。INSERT ... SELECTで埋めると、同じ行でも不合格になります。

細かく分けすぎたパーティション

同じ列でPARTITION BY (toDate(ts), region)・ORDER BY tsのptn.sales_fineを作成し、INSERT INTO ptn.sales_fine SELECT * FROM ptn.salesを実行して、出たエラー(stderr)を保存してください(保存先: /root/ch/partition/toofine.txt)。ptn.salesで日付と地域の組み合わせがいくつあるかを数えて、partitions_neededとして書き込んでください(保存先: /root/ch/partition/fine.json)。

1つのINSERTのブロックが作るパーティション数には上限(max_partitions_per_insert_block)があり、超えるとブロック全体が拒否されます。3か月×地域5つなら、いくつになるでしょうか。組み合わせの数はuniqExact(toDate(ts), region)で数えます。上限を上げて通してはいけません。