- テーブルには、どの時点でも重複 (同じソートキーを持つ行) が残っている可能性があります
- 重複行の実際の削除は、パーツのマージ時に行われます
- クエリでは、重複が存在する可能性を考慮する必要があります
重複排除のオプション
-
ReplacingMergeTreeテーブルエンジン: このテーブルエンジンでは、同じソートキーを持つ重複行がマージ時に削除されます。ReplacingMergeTreeは、upsert のような動作 (クエリで最後に挿入された行を返したい場合) を実現するのに適した選択肢です。 -
行の折りたたみ:
CollapsingMergeTreeおよびVersionedCollapsingMergeTreeテーブルエンジンでは、既存の行を「キャンセル」し、新しい行を挿入するというロジックを使用します。これらはReplacingMergeTreeより実装が複雑ですが、データがまだマージ済みかどうかを気にせず、クエリや集計をよりシンプルに記述できます。これら 2 つのテーブルエンジンは、データを頻繁に更新する必要がある場合に役立ちます。
upsert に ReplacingMergeTree を使う
views カラムを更新するには、同じ主キーを持つ新しい行を挿入します (views カラムの値が新しくなっている点に注目してください) :
SELECT クエリで FINAL キーワードを使ってみましょう。これにより、クエリ結果に対して論理的なマージが適用されます。
FINAL は、データ量が少ない場合であれば問題なく使えます。大量のデータを扱う場合、
FINAL の使用はおそらく最善の選択肢ではありません。カラムの最新の値を
取得する、よりよい方法を見ていきましょう。FINAL の使用を避ける
views カラムを、もう一度更新してみましょう。
FINAL を使った際のクエリ時のマージだけだからです) 。
FINAL を使う代わりに、ここではビジネスロジックを利用しましょう。views カラムは常に増加することがわかっているため、目的のカラムでグループ化したうえで、max 関数を使って最も大きい値を持つ行を選択できます。
FINAL キーワードを使用するよりも、実際には (クエリ性能の観点で) 効率的な場合があります。
この例については、Deleting and Updating Data training module でさらに詳しく解説しており、ReplacingMergeTree で version カラムを使用する方法も含まれています。
カラムを頻繁に更新するための CollapsingMergeTree の使用
ALTER TABLE..UPDATE を使うよりも、既存のデータに新しいデータを追加で insert するほうが、かえって効率的なことがあります。データが古いか新しいかを示すカラムを追加することもできますが、実はこの動作をうまく実現できる テーブルエンジン がすでにあります。しかも、古いデータは自動的に削除してくれます。では、どのように動作するのか見ていきましょう。
たとえば、外部システムを使って Hacker News のコメントの閲覧数を追跡し、そのデータを数時間ごとに ClickHouse に Push しているとします。古い行は削除され、新しい行が各 Hacker News コメントの新しい state を表すようにしたい場合、この動作は CollapsingMergeTree を使って実装できます。
閲覧数を保存する table を定義してみましょう。
hackernews_views テーブルには、sign という名前の Int8 カラムがあり、これを sign カラムと呼びます。sign カラムの名前自体は任意ですが、Int8 データ型であることは必須です。また、このカラム名が CollapsingMergeTree テーブルのコンストラクタに渡されている点にも注目してください。
CollapsingMergeTree テーブルの sign カラムとは何でしょうか。これはその行の state を表し、sign カラムに設定できる値は 1 または -1 のみです。動作は次のとおりです。
- 2 つの行が同じ主キー (主キーと異なる場合は同じソート順) を持ち、sign カラムの値だけが異なる場合、最後に +1 で挿入された行が state 行となり、他の行は互いに打ち消し合います
- 互いに打ち消し合う行は、マージ時に削除されます
- 対になる行がない行は保持されます
hackernews_views テーブルに 1 行追加してみましょう。この主キーに対応する行はこれだけなので、state を 1 に設定します。
(123, 'ricardo') の行が 3 行あります:
FINAL を追加すると、現在の state 行が返されます:
FINAL を使うことは推奨されません。
この例で
views カラムに渡している値は、実際には不要ですし、古い行の views の現在の値と一致している必要もありません。実際、主キーと -1 だけで行をキャンセルできます。複数スレッドからのリアルタイム更新
CollapsingMergeTree テーブルでは、sign カラムを使って行どうしが互いに打ち消し合い、行の状態は最後に挿入された行によって決まります。しかし、複数のスレッドから行を挿入していて、しかも行が順不同で挿入される可能性がある場合、これは問題になることがあります。このようなケースでは、「最後の」行を使う方法は機能しません。
そこで役に立つのが VersionedCollapsingMergeTree です。これは CollapsingMergeTree と同じように行を折りたたみますが、最後に挿入された行を保持する代わりに、指定したバージョンカラムの値が最も大きい行を保持します。
例を見てみましょう。Hacker News のコメントの閲覧数を追跡したいとします。このデータは頻繁に更新されます。マージを強制したり、その完了を待ったりすることなく、レポートでは最新の値を使いたいと考えています。まずは CollapsedMergeTree に似たテーブルから始めますが、行の状態のバージョンを保存するためのカラムを追加します:
VersionsedCollapsingMergeTree を使用し、sign カラム と version カラム を指定している点に注目してください。このテーブルの動作は次のとおりです。
- 同じ主キーとバージョンを持ち、sign が異なる行のペアはそれぞれ削除されます
- 行が挿入された順序は関係ありません
- version カラムが主キーの一部でない場合、ClickHouse はそれを最後のフィールドとして暗黙的に主キーへ追加する点に注意してください
hackernews_views_vcmt テーブルにいくつか行を追加してみましょう。
VersionedCollapsingMergeTree テーブルは非常に便利です。
行が重複排除されないのはなぜですか?
INSERT ステートメントで非冪等な関数または式を使用していることです。たとえば、createdAt DateTime64(3) DEFAULT now() というカラムを含む行を挿入している場合、各行の createdAt カラムには一意のデフォルト値が設定されるため、それらの行は必ず一意になります。MergeTree / ReplicatedMergeTree テーブルエンジンは、挿入された各行が一意のチェックサムを生成するため、それらの行を重複排除すべきものとして認識できません。
この場合、行の各 Batch に対して独自の insert_deduplication_token を指定することで、同じ Batch を複数回 insert しても同じ行が再度挿入されないようにできます。この設定の使い方の詳細については、insert_deduplication_token のドキュメントを参照してください。