BigQuery から ClickHouse Cloud への移行
なぜ BigQuery ではなく ClickHouse Cloud を使うのか?
要約すると、現代的なデータ分析においては、ClickHouse の方が BigQuery よりも高速で低コストかつ高機能だからです。

BigQuery から ClickHouse Cloud へのデータ読み込み
データセット
BigQuery から ClickHouse Cloud への典型的な移行例として、こちらで説明している Stack Overflow データセットを使用します。これは、2008 年から 2024 年 4 月までに Stack Overflow 上で発生したすべての post、vote、user、comment、badge を含みます。このデータに対する BigQuery のスキーマは次のとおりです:

移行手順をテストするために、このデータセットを BigQuery インスタンスに投入したいユーザー向けに、これらのテーブル用のデータを Parquet 形式で GCS バケットに用意しており、BigQuery でテーブルを作成およびロードするための DDL コマンドはこちらで利用できます。
データの移行
BigQuery と ClickHouse Cloud 間のデータ移行は、主に次の 2 種類のワークロードに分類できます:
- 初回一括ロード + 定期更新 - 初期データセットを移行し、その後は日次などの一定間隔で定期的に更新します。ここでの更新は、変更された行を再送信することで処理します。変更の識別には、(日付などの)比較に使用できるカラムを利用します。削除は、データセット全体を定期的に完全リロードすることで対応します。
- リアルタイムレプリケーションまたは CDC - 初期データセットを移行する必要があります。その後、このデータセットへの変更は、数秒程度の遅延のみ許容される形で、ほぼリアルタイムに ClickHouse に反映される必要があります。これは実質的に Change Data Capture (CDC) プロセスであり、BigQuery のテーブルが ClickHouse と同期されている必要があります。すなわち、BigQuery テーブルでの挿入(INSERT)・更新(UPDATE)・削除(DELETE)を、ClickHouse の同等のテーブルに適用しなければなりません。
Google Cloud Storage (GCS) 経由の一括ロード
BigQuery は、データを Google のオブジェクトストア(GCS)へエクスポートする機能をサポートしています。今回のサンプルデータセットでは:
-
7 つのテーブルを GCS にエクスポートします。そのためのコマンドはこちらで利用できます。
-
データを ClickHouse Cloud にインポートします。そのために gcs テーブル関数を使用できます。DDL とインポートクエリはこちらで利用できます。なお、ClickHouse Cloud インスタンスは複数のコンピュートノードで構成されるため、
gcsテーブル関数の代わりに s3Cluster テーブル関数を使用しています。この関数は GCS バケットでも動作し、ClickHouse Cloud サービスのすべてのノードを活用してデータを並列にロードします。

このアプローチには、次のような利点があります:
- BigQuery のエクスポート機能は、データのサブセットをエクスポートするためのフィルタをサポートしています。
- BigQuery は Parquet、Avro、JSON、CSV 形式および複数の圧縮形式へのエクスポートをサポートしており、いずれも ClickHouse でサポートされています。
- GCS はオブジェクト ライフサイクル管理をサポートしており、エクスポートして ClickHouse にインポートし終えたデータを、指定した期間後に削除できます。
- Google は 1 日あたり最大 50TB までの GCS へのエクスポートを無料で許可しています。ユーザーが支払うのは GCS ストレージ料金のみです。
- エクスポートは自動的に複数ファイルを生成し、各ファイルを最大 1GB のテーブルデータに制限します。これは、インポートを並列化できるため ClickHouse にとって有利です。
以下の例を試す前に、エクスポートとインポートのパフォーマンスを最大化するため、エクスポートに必要な権限およびロケーションに関する推奨事項を確認することを推奨します。
スケジュールされたクエリによるリアルタイムレプリケーションまたは CDC
Change Data Capture(CDC)は、2 つのデータベース間でテーブルを同期状態に保つプロセスです。更新および削除をほぼリアルタイムで扱う必要がある場合、処理は格段に複雑になります。1 つのアプローチとして、BigQuery のスケジュールされたクエリ機能を利用し、定期的なエクスポートを実行するようスケジュールする方法があります。ClickHouse へのデータ挿入に一定の遅延を許容できる場合、このアプローチは実装と保守が容易です。具体的な例はこのブログ記事で紹介されています。
スキーマの設計
Stack Overflow のデータセットには、関連する多数のテーブルが含まれています。まずは主要なテーブルの移行に集中することを推奨します。これは必ずしも最大のテーブルとは限らず、むしろ分析クエリの発行が最も多いと想定されるテーブルです。そうすることで、主要な ClickHouse の概念に慣れることができます。このテーブルは、追加のテーブルが増えるにつれて、ClickHouse の機能を最大限に活用し最適なパフォーマンスを得るために、再モデリングが必要になる場合があります。このモデリングプロセスについては、データモデリングのドキュメントで解説しています。
この原則に従い、ここではメインの posts テーブルに注目します。これに対する BigQuery のスキーマを以下に示します。
型の最適化
こちらで説明しているプロセスに従うと、次のようなスキーマになります。
INSERT INTO SELECT を使って、gcs table function により gcs からエクスポートされたデータを読み込み、このテーブルに簡単にデータを投入できます。なお、ClickHouse Cloud では、gcs 互換の s3Cluster table function を使用して、複数ノード間でロード処理を並列化することもできます。
新しいスキーマでは、NULL は一切保持しません。上記の INSERT 文により、これらはそれぞれの型のデフォルト値に暗黙的に変換されます。整数であれば 0、文字列であれば空文字列です。ClickHouse は数値型についても、自動的にターゲットの精度に変換します。
ClickHouse のプライマリキーは何が違うのか
こちらで説明したとおり、BigQuery と同様に、ClickHouse はテーブルのプライマリキー列の値に対して一意性を強制しません。
BigQuery におけるクラスタリングと同様に、ClickHouse のテーブルデータは、プライマリキー列を基準としてディスク上に順序づけて格納されます。このソート順は、クエリオプティマイザによって活用され、再ソートの回避、結合におけるメモリ使用量の最小化、および LIMIT 句の評価の早期打ち切りを可能にします。 BigQuery と異なり、ClickHouse はプライマリキー列の値に基づいて(疎な)プライマリインデックスを自動的に作成します。このインデックスは、プライマリキー列に対してフィルタを含むすべてのクエリの高速化に使用されます。具体的には次のとおりです。
- ClickHouse がよく利用されるようなスケールでは、メモリとディスクの効率性が極めて重要です。データは、パーツと呼ばれるチャンク単位で ClickHouse のテーブルに書き込まれ、バックグラウンドでパーツをマージするためのルールが適用されます。ClickHouse では、各パーツが自身のプライマリインデックスを持ちます。パーツがマージされると、マージ後のパーツのプライマリインデックスもマージされます。これらのインデックスは行ごとには構築されないことに注意してください。代わりに、1 つのパーツのプライマリインデックスには、行のグループごとに 1 件のインデックスエントリがあります。この手法は疎インデックスと呼ばれます。
- 疎インデックスが可能なのは、ClickHouse がパーツ内の行を、指定されたキーで順序づけた状態でディスクに格納しているためです。単一行を直接特定する(B-Tree ベースのインデックスのような)代わりに、疎なプライマリインデックスはインデックスエントリに対する二分探索により、クエリにマッチする可能性のある行グループを素早く特定できるようにします。特定された、マッチする可能性のある行グループは、その後並列で ClickHouse エンジンにストリーミングされ、実際にマッチする行の探索が行われます。このインデックス設計により、プライマリインデックスは小さく(メインメモリに完全に収まる状態に)保たれながらも、特にデータ分析ユースケースで典型的なレンジクエリにおいて、クエリ実行時間を大幅に短縮できます。詳細については、この詳細ガイドを参照することを推奨します。

ClickHouse で選択したプライマリキーは、インデックスだけでなく、データがディスクに書き込まれる順序も決定します。このため、圧縮率に大きな影響を与え、それが結果としてクエリ性能に影響を与える可能性があります。ほとんどの列の値が連続した順序で書き込まれるような並べ替えキーを選択すると、選択された圧縮アルゴリズム(およびコーデック)がデータをより効果的に圧縮できるようになります。
テーブル内のすべての列は、指定した並べ替えキーの値に基づいてソートされ、その列がキー自体に含まれているかどうかに関わらず、このルールが適用されます。たとえば、
CreationDateがキーとして使用される場合、他のすべての列の値の並び順は、CreationDate列の値の順序に対応します。複数の並べ替えキーを指定することもでき、その場合はSELECTクエリのORDER BY句と同じセマンティクスでソートされます。
並べ替えキーの選択
並べ替えキーを選択する際の考慮事項と手順については、posts テーブルを例にとったこちらを参照してください。
データモデリング手法
BigQuery から移行するユーザーは、まず ClickHouse におけるデータモデリングガイド を参照することを推奨します。このガイドでは同じ Stack Overflow データセットを使用し、ClickHouse の機能を活用した複数のアプローチを解説しています。
パーティション
BigQuery ユーザーは、大規模なデータベースにおいて、テーブルを「パーティション」と呼ばれる小さく扱いやすい単位に分割することで、性能と管理性を向上させるテーブルパーティショニングの概念に馴染みがあるはずです。パーティショニングは、指定したカラム(例: 日付)に対する範囲、定義済みリスト、あるいはキーに対するハッシュによって実現できます。これにより、管理者は日付範囲や地理的位置といった特定の条件に基づいてデータを整理できます。
パーティショニングは、パーティションプルーニングや、より効率的なインデックス付けにより、高速なデータアクセスを可能にし、クエリ性能を向上させます。また、テーブル全体ではなく個々のパーティション単位で操作できるため、バックアップやデータ削除といったメンテナンスタスクにも役立ちます。さらに、負荷を複数のパーティションに分散することで、BigQuery データベースのスケーラビリティを大幅に向上させることができます。
ClickHouse では、パーティショニングはテーブルを最初に定義する際に PARTITION BY 句によって指定します。この句には、任意のカラムに対する SQL 式を含めることができ、その評価結果によって各行がどのパーティションに送られるかが決まります。

データパーツはディスク上でそれぞれのパーティションに論理的に関連付けられ、パーティション単位で個別にクエリできます。以下の例では、toYear(CreationDate) という式を使用して posts テーブルを年ごとにパーティション化しています。行が ClickHouse に挿入される際、この式が各行に対して評価され、その結果に基づき、そのパーティションに属する新しいデータパーツとして適切なパーティションにルーティングされます。
用途
ClickHouse におけるパーティショニングの用途は BigQuery と似ていますが、いくつか細かな違いがあります。具体的には次のとおりです。
- データ管理 - ClickHouse では、パーティショニングはクエリ最適化の手段ではなく、主にデータ管理のための機能として捉えるべきです。キーに基づいてデータを論理的に分割することで、それぞれのパーティションを独立して操作(例: 削除)できます。これにより、ストレージ階層間でパーティション、ひいてはその部分集合を、時間ベースで効率的に移動したり、クラスタからのデータの期限切れ/効率的な削除を行ったりできます。例えば、以下では 2008 年の投稿を削除しています。
- クエリ最適化 - パーティションはクエリパフォーマンスの改善に役立つ場合がありますが、その効果はアクセスパターンに大きく依存します。クエリが少数のパーティション(理想的には 1 つ)のみを対象とする場合、パフォーマンスが向上する可能性があります。これは通常、パーティショニングキーがプライマリキーに含まれておらず、かつそのキーでフィルタリングしている場合にのみ有用です。一方で、多数のパーティションをまたいで読み取る必要があるクエリは、パーティショニングを行わない場合よりもパフォーマンスが低下する可能性があります(パーティショニングの結果として
partsが増える可能性があるため)。対象を 1 つのパーティションに限定できることによる利点も、パーティショニングキーがすでにプライマリキーの先頭付近にある場合にはほとんど、あるいはまったくと言ってよいほど小さくなります。パーティショニングは、各パーティション内の値が一意である場合に限り、GROUP BYクエリを最適化 するためにも利用できます。ただし、一般的には、まずプライマリキーが最適化されていることを確認し、そのうえで、アクセスパターンが 1 日の中の特定の予測可能なサブセットだけにアクセスするような例外的なケース(例: 1 日単位でパーティションを切り、ほとんどのクエリが直近 1 日のみを対象とする場合)に限って、クエリ最適化手法としてのパーティショニングを検討すべきです。
推奨事項
パーティショニングはデータ管理のテクニックと考えるべきです。特に時系列データを扱う際、クラスターから古いデータを削除する必要がある場合に適しています。例えば、最も古いパーティションを単純に削除することができます。
重要: パーティショニングキーの式によって高カーディナリティな集合が生成されないようにしてください。すなわち、100 を超えるパーティションを作成することは避けてください。例えば、クライアント識別子や名前のような高カーディナリティの列でデータをパーティション分割しないでください。その代わりに、クライアント識別子や名前を ORDER BY 式の先頭の列にしてください。
内部的には、ClickHouse は挿入されたデータに対してparts を作成します。より多くのデータが挿入されると、parts の数は増加します。あまりに多くの parts が存在すると、読み取るファイル数が増えるためクエリパフォーマンスが低下してしまいますが、これを防ぐために、バックグラウンドの非同期処理で parts がマージされます。parts の数が事前に設定された上限を超えると、ClickHouse は挿入時に例外をスローし、「too many parts」エラーとして扱います。これは通常の運用では発生せず、ClickHouse の設定ミスや誤った使用方法(例: 非常に小さい挿入を多数行う)によってのみ発生します。parts は各パーティションごとに独立して作成されるため、パーティション数を増やすと parts の数も増加し、パーティション数に比例して大きくなります。そのため、高カーディナリティなパーティショニングキーはこのエラーの原因となりうるため、避けるべきです。
マテリアライズドビューとプロジェクション
ClickHouse のプロジェクションの概念により、ユーザーは 1 つのテーブルに対して複数の ORDER BY 句を指定できます。
ClickHouse のデータモデリング では、マテリアライズドビューを ClickHouse でどのように活用して、集約の事前計算や行の変換を行い、さまざまなアクセスパターンに応じてクエリを最適化できるかを解説しています。このうち、アクセスパターンごとのクエリ最適化については、マテリアライズドビューが、挿入を受け取る元のテーブルとは異なるソートキーを持つターゲットテーブルへ行を送る、という具体例を示しました。
例えば、次のクエリを考えてみます。
このクエリは、UserId がソートキーではないため(高速ではあるものの)9,000万行すべてをスキャンする必要があります。以前は、PostId のルックアップとして機能するマテリアライズドビューを使って、この問題を解決していました。同じ問題はプロジェクションでも解決できます。
以下のコマンドは、ORDER BY user_id を持つプロジェクションを追加します。
まずプロジェクションを作成し、その後にマテリアライズする必要がある点に注意してください。 後者のコマンドを実行すると、データは異なる順序でディスク上に 2 回保存されます。 プロジェクションは、以下に示すようにデータを作成する際に定義することもでき、 データが挿入されると自動的に維持されます。
ALTER コマンドでプロジェクションを作成した場合、MATERIALIZE PROJECTION コマンドを発行しても、その作成は非同期で行われます。次のクエリでこの処理の進捗を確認し、is_done=1 になるまで待機できます。
上記のクエリを再実行すると、追加のストレージを要するものの、パフォーマンスが大幅に向上していることが分かります。
EXPLAIN コマンド を使用して、このクエリの処理にプロジェクションが利用されたことも確認できます。
┌─explain─────────────────────────────────────────────┐
- │ 式 ((Projection + Before ORDER BY)) │
- │ 集約 │
- │ フィルター │
- │ ReadFromMergeTree (comments_user_id) │
- │ インデックス: │
- │ PrimaryKey │
- │ キー: │
- │ UserId │
- │ 条件: (UserId in [8592047, 8592047]) │
- │ パーツ: 2/2 │
- │ グラニュール: 2/11360 │ └─────────────────────────────────────────────────────┘
11 行が結果セットに含まれています。経過時間: 0.004 秒。
ClickHouse 向けの BigQuery クエリの書き換え
以下は、BigQuery と ClickHouse のクエリを比較したサンプルクエリです。このリストは、ClickHouse の機能を活用してクエリを大幅に簡素化する方法を示すことを目的としています。ここでの例では、Stack Overflow の全データセット(2024 年 4 月まで)を使用します。
質問を 10 件超投稿しているユーザーのうち、最も多くビューを獲得しているユーザー:
BigQuery

ClickHouse
どのタグの閲覧数が最も多いか:
BigQuery

ClickHouse
集約関数
可能な場合は、ClickHouse の集約関数を活用してください。以下では、各年でもっとも閲覧された質問を求めるために、argMax 集約関数 を使用する例を示します。
BigQuery


ClickHouse
条件式と配列
条件式と配列関数を使うと、クエリを大幅に簡潔にできます。次のクエリは、2022 年から 2023 年にかけての出現回数が 10000 回を超えるタグのうち、増加率が最も大きいものを算出します。以下の ClickHouse クエリが、条件式・配列関数・HAVING 句および SELECT 句でエイリアスを再利用できる機能のおかげで簡潔になっている点に注目してください。
BigQuery

ClickHouse
これで、BigQuery から ClickHouse へ移行するユーザー向けの基本ガイドは終了です。BigQuery から移行するユーザーは、ClickHouse の高度な機能についてさらに理解するために、ClickHouse におけるデータモデリング のガイドを読むことをお勧めします。