アーキテクチャ概要
This is the web version of our VLDB 2024 scientific paper. We also blogged about its background and journey, and recommend watching the VLDB 2024 presentation by ClickHouse CTO and creator, Alexey Milovidov:
ABSTRACT
過去数十年の間に、ストレージおよび分析されるデータの量は指数関数的に増加しました。業種やセクターにわたる企業は、このデータを利用して製品を改善し、パフォーマンスを評価し、ビジネスクリティカルな意思決定を行うことに依存し始めています。しかし、データのボリュームがインターネット規模になっていく中で、企業は歴史的データと新しいデータをコスト効率的かつスケーラブルな方法で管理し、高い同時クエリ数を使用してリアルタイムのレイテンシ(使用ケースによっては1秒未満)を期待しつつ分析しなければなりません。
この論文では、ペタバイト規模のデータセットに対して高性能な分析を目的として設計された人気のオープンソースOLAPデータベースであるClickHouseの概要を紹介します。そのストレージレイヤーは、従来のログ構造マージ(LSM)ツリーに基づくデータ形式と、バックグラウンドでの歴史的データの継続的変換(集約やアーカイブなど)を組み合わせたものです。クエリは便利なSQLダイアレクトで書かれ、最先端のベクトル化されたクエリ実行エンジンによって処理され、オプションでコードのコンパイルが行われます。ClickHouseは、クエリ内で無関係なデータを評価するのを避けるために、積極的にプルーニング技術を使用します。他のデータ管理システムは、テーブル関数、テーブルエンジン、またはデータベースエンジンレベルで統合できます。実際のベンチマークから、ClickHouseは市場で最も高速な分析データベースのひとつであることが示されています。
1 INTRODUCTION
この論文では、数兆行と数百カラムを対象とした高性能分析クエリ用に設計された列指向OLAPデータベースであるClickHouseを説明します。ClickHouseは2009年にウェブスケールのログファイルデータのフィルタと集約演算子として始まり、2016年にオープンソースとして公開されました。Figure 1は、この論文で説明される主要な機能がClickHouseに導入された時期を示しています。
ClickHouseは、現代の分析データ管理の5つの主要な課題に対応するように設計されています。
-
高い取り込みレートを持つ巨大データセット。ウェブ分析、金融、電子商取引などの業界における多くのデータ駆動型アプリケーションは、大量かつ継続的に増加するデータ量が特徴です。巨大データセットを扱うために、分析データベースは効率的なインデクシングと圧縮戦略を提供するだけでなく、単一のサーバーが数十テラバイトのストレージに制限されているため、複数のノード(スケールアウト)にデータを分散できる必要があります。さらに、最近のデータは歴史的データよりもリアルタイムの洞察にとってはるかに重要です。このため、分析データベースは、新しいデータを一貫して高いレートでまたはバーストで取り込む能力が必要であり、同時にパラレル報告クエリを遅延させずに歴史データを常に「優先度を下げる」必要があります(e.g. 集約、アーカイブ等)。
-
低レイテンシを期待する多数の同時クエリ。クエリは一般的に、アドホック(e.g. 探索的データ分析)または定期的(e.g. 定期的なダッシュボードクエリ)に分類できます。使用ケースがインタラクティブであればあるほど、クエリのレイテンシは低くなることが期待され、これがクエリの最適化や実行において課題を引き起こします。定期的なクエリは、ワークロードに合わせて物理データベースレイアウトを適応させる機会も提供します。その結果、データベースは、頻繁に実行されるクエリを最適化できるプルーニング技術を提供する必要があります。クエリの優先度に応じて、データベースはCPU、メモリ、ディスク、およびネットワークI/Oなどの共有システムリソースへの平等または優先的なアクセスを保証しなければなりません。
-
多様なデータストア、ストレージ場所、およびフォーマット。既存のデータアーキテクチャと統合するために、最新の分析データベースは、任意のシステム、場所、またはフォーマットで外部データを読み書きする高いオープン性を示すことが求められます。
-
パフォーマンスのインストロスペクションをサポートする便利なクエリ言語。OLAPデータベースの実際の使用は、追加の「ソフト」要件を示します。例えば、特異なプログラミング言語の代わりに、ユーザーはしばしばネストされたデータ型と幅広い通常の集約およびウィンドウ関数を使用して、表現力豊かなSQLダイアレクトでデータベースとインターフェースを持ちたいと考えます。分析データベースはまた、システムまたは個々のクエリのパフォーマンスを分析するための洗練されたツールも提供すべきです。
-
業界標準の堅牢性と多様なデプロイ。コモディティハードウェアは信頼性が低いため、データベースはノード障害に対する堅牢性のためのデータレプリケーションを提供しなければなりません。また、データベースは古いラップトップから強力なサーバーまで、あらゆるハードウェアで動作する必要があります。最後に、JVMベースのプログラムでガベージコレクションのオーバーヘッドを避け、ベアメタルパフォーマンス(e.g. SIMD)を可能にするために、データベースはターゲットプラットフォーム向けのネイティブバイナリとしてデプロイされるのが理想的です。

Figure 1: ClickHouseのタイムライン。
2 ARCHITECTURE

Figure 2: ClickHouseデータベースエンジンの高レベルアーキテクチャ。
Figure 2に示されているように、ClickHouseエンジンは3つの主要な層に分かれています:クエリ処理層(Section 4で説明)、ストレージ層(Section 3)、および統合層(Section 5)。これに加え、アクセス層はユーザーセッションを管理し、異なるプロトコルを介してアプリケーションと通信します。スレッド処理、キャッシング、ロールベースのアクセス制御、バックアップ、および継続的監視のための独立したコンポーネントがあります。ClickHouseは、C++で構築され、依存関係のない単一の静的リンクバイナリとして提供されます。
クエリ処理は、受信したクエリの解析、論理および物理クエリプランの構築と最適化、実行という従来のパラダイムに従います。ClickHouseは、MonetDB/X100 [11]と類似のベクトル化された実行モデルを使用しており、機会主義的なコードコンパイル [53] と組み合わせています。クエリは、機能が豊富なSQLダイアレクトで記述、PRQL [76]、またはKustoのKQL [50]で処理できます。
ストレージ層は、テーブルデータの形式と場所をカプセル化した異なるテーブルエンジンで構成されています。テーブルエンジンは、3つのカテゴリに分類されます:最初のカテゴリは、ClickHouseでの主要な永続性形式を表すMergeTreeエンジンのファミリーです。LSMツリー [60]のアイデアに基づき、テーブルは横方向にソートされたパーツに分割され、バックグラウンドプロセスによって連続的にマージされます。個々のMergeTreeテーブルエンジンは、その入力パーツから行を合わせる方法において異なります。例えば、行は集約されるか、古くなった場合は置き換えられます。
2番目のカテゴリは、クエリ実行を高速化または分散するために使用される特別な目的のテーブルエンジンです。このカテゴリには、内部または外部データソースに対して定期的に実行されるクエリの結果をキャッシュする辞書と呼ばれるメモリ内のキー値テーブルエンジンが含まれます。辞書は、データの鮮度が優先されるシナリオで、アクセスレイテンシを大幅に低下させます。他の特別目的のテーブルエンジンの例には、テンポラリーテーブル用の純粋なメモリ内エンジンや、透明なデータシャーディングのための分散テーブルエンジンが含まれます。
3番目のカテゴリのテーブルエンジンは、関係データベース(e.g. PostgreSQL、MySQL)、パブリッシュ/サブスクライブシステム(e.g. Kafka、RabbitMQ [24])、またはキー/値ストア(e.g. Redis)との双方向データ交換のための仮想テーブルエンジンです。仮想エンジンは、データレイク(e.g. Iceberg、DeltaLake、Hudi [36])やオブジェクトストレージ内のファイル(e.g. AWS S3、Google GCP)とも対話できます。
ClickHouseは、スケーラビリティと可用性のために、複数のクラスターノードにわたるテーブルのシャーディングとレプリケーションをサポートしています。シャーディングは、シャーディング式に従ってテーブルを一連のテーブルシャードに分割します。各シャードは独立したテーブルであり、通常は異なるノードに配置されています。クライアントは、シャードに直接読み書きでき、つまりそれらを別々のテーブルとして扱うことができ、あるいはすべてのテーブルシャードのグローバルビューを提供する分散特別テーブルエンジンも使用できます。シャーディングの主な目的は、個々のノードの容量を超えるデータセットを処理することです(通常は数十テラバイトのデータ)。シャーディングの別の用途は、テーブルの読み書き負荷を複数のノードに分散させ、つまり負荷分散を行うことです。それとは別に、シャードはノード障害に対する耐障害性のために複数のノードに複製できます。このために、各Merge-Treeテーブルエンジンには対応するReplicatedMergeTreeエンジンがあり、Raftコンセンサスに基づくマルチマスター調整方式を利用します [59] (Apache Zookeeperの代替品としてC++で書かれたKeeperに実装されています)を利用して、各シャードが常に設定可能な数のレプリカを持つことを保証します。Section 3.6では、レプリケーションメカニズムについて詳しく説明します。例えば、Figure 2は、2つのシャードがあり、各シャードが2つのノードにレプリケートされるテーブルを示しています。
最後に、ClickHouseデータベースエンジンは、オンプレミス、クラウド、スタンドアロン、またはプロセス内モードで操作できます。オンプレミスモードでは、ユーザーはClickHouseを単一のサーバーまたは複数のノードクラスタとしてローカルにセットアップし、シャーディングおよび/またはレプリケーションを行います。クライアントは、ネイティブ、MySQL、PostgreSQLのバイナリワイヤプロトコル、またはHTTP REST APIを介してデータベースと通信します。クラウドモードは、完全に管理され、自動スケーリングされるDBaaS提供のClickHouse Cloudが含まれます。この論文はオンプレミスモードに焦点を当てていますが、ClickHouse Cloudのアーキテクチャを次回の発表で説明する予定です。スタンドアロンモードでは、ClickHouseをファイルの分析と変換のためのコマンドラインユーティリティに変えることができ、catやgrepのようなUnixツールに対するSQLベースの代替手段となります。この設定には事前の構成が不要ですが、スタンドアロンモードは単一のサーバーに制限されています。最近、Jupyterノートブック [37] やPandasデータフレーム [61]のようなインタラクティブなデータ分析使用ケースのために、chDBと呼ばれるプロセス内モードが開発されました [15]。DuckDB [67]からインスパイアを受けて、chDB はClickHouseをホストプロセスに高性能OLAPエンジンとして組み込みます。他のモードと比較して、これにより、データベースエンジンとアプリケーション間でソースおよび結果データを効率良く渡すことが可能になり、同じアドレス空間内で実行されます。
3 STORAGE LAYER
このセクションでは、ClickHouseのネイティブストレージ形式としてのMergeTree*テーブルエンジンについて説明します。ディスク上の表現を説明し、ClickHouseにおける3つのデータプルーニング技術について議論します。その後、同時挿入に影響を与えずにデータを継続的に変換するためのマージ戦略を紹介します。最後に、更新や削除がどのように実装されているか、データの重複排除、データレプリケーション、ACID準拠について説明します。
3.1 On-Disk Format
MergeTree*テーブルエンジンの各テーブルは、不変のテーブルパーツのコレクションとして整理されています。パートは、行のセットがテーブルに挿入されるたびに作成されます。パーツは自己完結型であり、追加の中央カタログのルックアップなしに、その内容を解釈するために必要なすべてのメタデータを含みます。テーブルごとにパーツの数を低く保つために、バックグラウンドマージジョブが定期的に複数の小さなパーツを結合して、設定可能なパーツサイズに達するまで大きなパーツにします(デフォルトは150 GB)。パーツはテーブルの主キー列によってソートされているため(Section 3.2参照)、効率的なk-wayマージソート [40]がマージに使用されます。ソースパーツは非アクティブとしてマークされ、最終的に参照カウントがゼロ(すなわち、もうそれらから読み取るクエリがない)になると削除されます。
行は二つのモードで挿入できます:同期挿入モードでは、各INSERT文が新しいパートを作成し、テーブルに追加します。マージのオーバーヘッドを最小限に抑えるために、データベースクライアントは一度に例として20,000行を一括で挿入することが奨励されます。しかし、クライアント側のバッチ処理によって引き起こされる遅延は、データをリアルタイムで分析する必要がある場合にはしばしば受け入れられません。例えば、可観測性の使用ケースは、通常、数千のモニタリングエージェントが小さなイベントおよびメトリクスデータを継続的に送信することが含まれます。このようなシナリオでは、ClickHouseが複数の受信INSERTからの行を同じテーブルにバッファリングし、バッファサイズが設定可能なしきい値を超えるかタイムアウトが期限切れになるまで新しいパートを作成しない非同期挿入モードを利用できます。

Figure 3: MergeTree*-エンジンテーブルの挿入とマージ。
Figure 3は、MergeTree*-エンジンテーブルへの4つの同期および2つの非同期挿入を示しています。2つのマージによって、アクティブなパーツの数は最初の5から2に減少しました。
LSMツリー [58]およびそれらのさまざまなデータベースにおける実装 [13, 26, 56] と比較して、ClickHouseはすべてのパーツを等しく扱い、階層を形成しません。その結果、マージの制限は同じレベルのパーツに留まらなくなります。これはパーツの暗黙の時間的順序付けも放棄するため、トゥームストーンに基づく更新および削除の代替メカニズムが必要です(Section 3.4参照)。ClickHouseは、挿入をディスクに直接書き込みますが、他のLSMツリーに基づくストアは通常、先行書き込みログを使用します(Section 3.7参照)。
一つのパートはディスク上のディレクトリに対応し、各カラムのための1つのファイルを含みます。最適化として、小さなパート(デフォルトでは10 MB未満)のカラムは、読み書きの空間的局所性を高めるために、単一のファイルに連続して保存されます。パートの行はさらに8192のレコードのグループに論理的に分割され、これは「グラニュール」と呼ばれます。グラニュールは、ClickHouseにおけるスキャンおよびインデックスルックアップオペレーターによって処理される最小の分割不可能なデータユニットを表します。ただし、ディスク上のデータの読み取りおよび書き込みは、グラニュールレベルではなく、カラム内の隣接するグラニュールを結合するブロックの粒度で行われます。新しいブロックは設定可能なバイトサイズごとに形成されます(デフォルトは1 MB)、すなわち、ブロック内のグラニュールの数は可変で、カラムのデータ型および分布に依存します。ブロックはさらに、そのサイズとI/Oコストを削減するために圧縮されます。デフォルトでは、ClickHouseは一般的な圧縮アルゴリズムとしてLZ4 [75]を使用しますが、ユーザーはGorilla [63]やFPC [12]のような特殊なコーデックを浮動小数点データ用に指定することもできます。圧縮アルゴリズムはチェーン化することもできます。例えば、最初に数値の論理的冗長性をデルタコーディング [23] を使用して削減し、次に重みのある圧縮を実行し、最後にAESコーデックを使用してデータを暗号化することが可能です。ブロックは、ディスクからメモリにロードされる際にリアルタイムで解凍されます。圧縮にもかかわらず個々のグラニュールへの迅速なランダムアクセスを可能にするために、ClickHouseはまた、各カラムに対して、すべてのグラニュールIDをカラムファイル内の圧縮ブロックのオフセットおよび未圧縮ブロック内のグラニュールのオフセットに関連付けるマッピングを保存します。
カラムはさらに辞書エンコード [2, 77, 81] されるか、Nullableを使用して作成可能です:LowCardinality(T) は、元のカラム値を整数IDに置き換え、ユニークな値が少ないデータのストレージオーバーヘッドを大幅に削減します。Nullable(T) は、カラムTに対して、カラム値がNULLかどうかを表す内部ビットマップを追加します。
最後に、テーブルは範囲、ハッシュ、またはラウンドロビンで、任意の分割式を使用してパーティション分割できます。パーティションプルーニングを可能にするために、ClickHouseはさらに各パーティションのパーティショニング式の最小値と最大値を保存します。ユーザーは、ハイパーログログ [30] やt-digest [28]統計などのより高度なカラム統計(例えば、基数推定を提供するもの)をオプションで作成できます。
3.2 Data Pruning
ほとんどの使用ケースでは、ペタバイトのデータをスキャンして単一のクエリに回答するのは遅すぎて高価です。ClickHouseは、検索中に大多数の行をスキップできる3つのデータプルーニング技術をサポートしています。
最初に、ユーザーはテーブルのために主キーインデックスを定義できます。主キー列は、各パート内の行のソート順序を決定し、すなわちインデックスはローカルにクラスタリングされます。ClickHouseはさらに、各グラニュールの最初の行の主キー列値からグラニュールのIDへのマッピングを各パートのために保存します。すなわち、インデックスはスパースです [31]。結果として得られるデータ構造は通常、すべてをメモリ内に保持できる程度に小さく、例えば8.1百万の行をインデックス化するのにわずか1000エントリが必要です。主キーの主な目的は、頻繁にフィルタされる列に対する等価性と範囲の述語を、逐次スキャンの代わりにバイナリ検索を使用して評価することです(Section 4.4参照)。また、ローカルソートは、パートマージやクエリ最適化に利用可能であり、例えば、ソートベースの集約や、主キー列がソート列の接頭辞を形成する場合には、物理実行計画からソートオペレーターを取り除くことができます。
Figure 4は、ページインプレッション統計のテーブルに対する列EventTimeの主キーインデックスを示しています。クエリ中の範囲述語に一致するグラニュールは、EventTimeを逐次スキャンする代わりに主キーインデックスでバイナリ検索することによって見つけることができます。

Figure 4: 主キーインデックスによるフィルタ評価。
次に、ユーザーはテーブルプロジェクションを作成できます。すなわち、異なる主キーによってソートされている同じ行を含むテーブルの別バージョンです [71]。プロジェクションは、主テーブルの主キーとは異なる列でフィルタリングされるクエリを高速化し、挿入、マージ、スペース消費のオーバーヘッドを増加させます。デフォルトでは、プロジェクションは主テーブルへの新しい挿入からのみ遅延的に生成され、ユーザーがプロジェクションを完全に具現化しない限り既存のパーツからは生成されません。クエリオプティマイザーは、推定I/Oコストに基づいて主テーブルまたはプロジェクションのいずれから読むかを選択します。あるパートに対するプロジェクションが存在しない場合、クエリ実行は対応する主テーブルパートにフォールバックします。
第三に、スキップインデックスは、プロジェクションに対する軽量の代替手段を提供します。スキップインデックスのアイデアは、無関係な行のスキャンを避けることができる小さなメタデータを複数の連続するグラニュールのレベルで保存することです。スキップインデックスは、任意のインデックス式のために作成でき、設定可能なグラニュラリティ、すなわちスキップインデックスブロック内のグラニュール数を使用できます。利用可能なスキップインデックスタイプには、1. min-maxインデックス [51] があり、これは各インデックスブロックのインデックス式の最小値と最大値を保存します。このインデックスタイプは、絶対的な範囲が小さなローカルクラスタデータに対してよく機能します。2. 固定数のユニークインデックスブロック値を保存するセットインデックス。これらのインデックスは、ローカル基数が小さい、すなわち「集中的にまとめられた」値を持つデータで最も良く使用されます。3. トークンやn-グラム値に対して設定可能な偽陽性率で構築されたブルームフィルターインデックス [9]。これらのインデックスはテキスト検索をサポートしますが [73]、min-maxおよびセットインデックスとは異なり、範囲または負の述語には使用できません。
3.3 Merge-time Data Transformation
ビジネスインテリジェンスや可観測性の使用ケースは、高率またはバーストで生成されるデータを処理する必要があります。そして、最近生成されたデータは通常、歴史的データよりも意味のあるリアルタイムの洞察に関連性が高いです。このような使用ケースでは、データベースは高いデータ取り込みレートを維持しながら、集約やデータの老朽化といった技術を通じて歴史的データのボリュームを継続的に削減する必要があります。ClickHouseは、さまざまなマージ戦略を使用して、既存のデータを継続的にインクリメンタル変換することを可能にします。マージ時のデータ変換はINSERT文のパフォーマンスを妨げることはありませんが、テーブルが常に不要な(e.g. 古いまたは集約されていない)値を含まないことを保証することはできません。必要に応じて、すべてのマージ時変換はSELECT文でFINALキーワードを指定することで、クエリ時に適用できます。
置き換えマージは、含まれるパートの作成タイムスタンプに基づいて、タプルの最も最近挿入されたバージョンのみを保持し、古いバージョンは削除されます。タプルは、同じ主キー列値を持っている場合に等価と見なされます。どのタプルが保持されるかを明示的に制御するために、比較のための特別なバージョンカラムを指定することも可能です。置き換えマージは、通常、頻繁に更新される使用ケースでのマージ時アップデートメカニズムとして、あるいは挿入時のデータ重複排除の代替手段として使用されます(Section 3.5参照)。
集約マージは、同じ主キー列値を持つ行を集約された行にまとめます。非主キー列は、要約値を保持する部分的集約状態である必要があります。平均(avg())のための合計とカウントなど、2つの部分的集約状態を新しい部分的集約状態に結合します。集約マージは、通常のテーブルではなく、マテリアライズドビューで使用されます。マテリアライズドビューは、ソーステーブルに対する変換クエリに基づいてポピュレートされます。他のデータベースとは異なり、ClickHouseはマテリアライズドビューをソーステーブルの全内容で周期的に更新しません。代わりに、マテリアライズドビューは、新しいパートがソーステーブルに挿入されたときに、変換クエリの結果でインクリメンタルに更新されます。
Figure 5は、ページインプレッション統計のテーブルに対して定義されたマテリアライズドビューを示します。ソーステーブルに挿入された新しいパートについて、変換クエリは、地域ごとに最大及び平均レイテンシを計算し、その結果をマテリアライズドビューに挿入します。集約関数avg()およびmax()は、実際の結果の代わりに部分的集約状態を返します。マテリアライズドビューのために定義された集約マージは異なるパーツ内の部分的集約状態を継続的に結合します。最終的な結果を得るために、ユーザーはavg()およびmax()を使用してマテリアライズドビュー内の部分的集約状態を統合します。

Figure 5: マテリアライズドビュー内の集約マージ。
有効期限(TTL)マージは、歴史的データの老朽化を提供します。削除マージや集約マージとは異なり、TTLマージは一度に1つのパートのみを処理します。TTLマージは、トリガーとアクションを持つルールの観点で定義されます。トリガーは、各行のためのタイムスタンプを計算する式であり、TTLマージが実行される時間と比較されます。これにより、ユーザーは行の粒度でアクションを制御できますが、条件を満たす行がすべて満たしているかどうかを確認し、パート全体にアクションを実行することが十分であると認識しました。可能なアクションには、1. パートを他のボリュームに移動する(例:安価で遅いストレージ)、2. パートを再圧縮する(例:より重いコーデックで)、3. パートを削除する、4. ロールアップ(すなわち、グルーピングキーおよび集約関数を使用して行を集約する)が含まれます。
例えば、Listing 1に見られるロギングテーブル定義を考えましょう。ClickHouseは、タイムスタンプ列の値が1週間以上古いパーツを遅いが安価なS3オブジェクトストレージに移動します。
Listing 1: 1週間後にパートをオブジェクトストレージに移動。
3.4 Updates and Deletes
MergeTree*テーブルエンジンの設計は、追加専用のワークロードを優先していますが、一部の使用ケースでは時折既存データを変更する必要があります。更新または削除のデータには、どちらも並行挿入をブロックせずに行う二つのアプローチがあります。
ミューテーションは、テーブル内のすべてのパーツをインプレースで再書き込みます。テーブル(削除)やカラム(更新)が一時的にサイズを倍増するのを防ぐため、この操作は非原子的です。すなわち、並行実行のSELECT文は、変異したパーツと未変異のパーツを読むことができます。ミューテーションは、操作の終了時にデータが物理的に変更されることを保証します。ただし、削除ミューテーションは、すべてのカラムをすべてのパーツに書き直すため、依然として高コストです。
代替手段として、軽量削除は、行が削除されているかどうかを示す内部ビットマップカラムのみを更新します。ClickHouseは、削除された行を結果から除外するために、ビットマップカラムに対する追加フィルタを使用してSELECTクエリを修正します。削除された行は、将来の未指定の時間に通常のマージによって物理的に削除されます。カラム数に応じて、軽量削除はミューテーションよりもはるかに高速で行うことができ、SELECTは遅くなります。
同じテーブルに対する更新および削除操作は、論理的な矛盾を避けるために、まれで直列化されることが期待されます。
3.5 Idempotent Inserts
実際に頻繁に発生する問題の一つは、クライアントがデータをテーブルに挿入するためにサーバーに送信した後の接続タイムアウトをどのように処理するかです。この状況では、クライアントはデータが正常に挿入されたかどうかを区別するのが難しくなります。この問題は、クライアントからサーバーへのデータの再送信によって解決され、主キーまたはユニーク制約によって重複した挿入を拒否することに依存しています。データベースは、バイナリツリー [39, 68]、基数木 [45]、またはハッシュテーブル [29] に基づいたインデックス構造を使用して、必要なポイントルックアップを迅速に実行します。これらのデータ構造は各タプルをインデックス化するため、そのスペースおよび更新オーバーヘッドは大規模なデータセットおよび高取り込みレートでは過剰になります。
ClickHouseは、各挿入が最終的にパーツを作成するという事実に基づいた軽量の代替手段を提供します。具体的には、サーバーは最後に挿入されたN個のパーツのハッシュを保持し(例としてN=100)、既知のハッシュを持つパーツの再挿入を無視します。非レプリケートテーブルとレプリケートテーブルのハッシュは、それぞれKeeperにローカルに保存されます。その結果、挿入は冪等になります。すなわち、クライアントはタイムアウト後に同じ行のバッチを再送信するだけで、サーバーが重複排除を処理することが期待できます。重複排除プロセスをより厳密に制御するために、クライアントはオプションでパートハッシュとして機能する挿入トークンを提供することができます。ハッシュベースの重複排除は、新しい行をハッシュ化する際のオーバーヘッドを伴いますが、ハッシュを保存して比較するコストは無視できるものです。
3.6 データレプリケーション
レプリケーションは高可用性(ノードの障害に対する耐障害性)に必要不可欠ですが、負荷分散やゼロダウンタイムアップグレードのためにも使用されます [14]。ClickHouseでは、レプリケーションはテーブルの状態という概念に基づいており、この状態はテーブルパーツのセット(セクション 3.1) と、カラム名やタイプなどのテーブルメタデータで構成されています。ノードは、以下の3つの操作を使用してテーブルの状態を進めます:1. 挿入は状態に新しいパーツを追加、2. マージは新しいパーツを追加し、状態から既存のパーツを削除、3. ミューテーションやDDLステートメントはパーツの追加、削除、およびテーブルメタデータの変更を行います。操作は単一ノード上でローカルに実行され、その状態遷移はグローバルなレプリケーションログに記録されます。
レプリケーションログは通常3つのClickHouse Keeperプロセスのアンサンブルによって維持され、Raft合意アルゴリズム [59] を利用して、ClickHouseノードのクラスターに対する分散型で耐障害性のある調整層を提供します。すべてのクラスターノードは最初にレプリケーションログの同じ位置を指します。ノードはローカルな挿入、マージ、ミューテーション、DDLステートメントを実行する間、レプリケーションログは他のすべてのノードで非同期的に再生されます。その結果、レプリケートされたテーブルは最終的には一貫性があります。つまり、ノードは最新の状態に収束している間、一時的に古いテーブルの状態を読み取ることができます。前述のほとんどの操作は、ノードの過半数(例:大多数のノードまたはすべてのノード)が新しい状態を採用するまで同期的に実行されることもあります。
例として、図6は3つのClickHouseノードのクラスター内に存在する初期状態の空のレプリケートテーブルを示しています。ノード1は最初に2つのINSERTステートメントを受け取り、これをレプリケーションログ(1 2)に記録します。次に、ノード2は最初のログエントリーを取得して再生し(3)、ノード1から新しいパーツをダウンロードします(4)。ノード3は両方のログエントリーを再生します(3 4 5 6)。最後に、ノード3は両方のパーツを新しいパーツにマージし、入力パーツを削除し、レプリケーションログにマージエントリーを記録します(7)。

図6: 三つのノードのクラスターにおけるレプリケーション。
同期化をスピードアップするための3つの最適化があります。まず、クラスターに追加された新しいノードは、レプリケーションログを最初から再生するのではなく、最後のレプリケーションログエントリーを書いたノードの状態を単にコピーします。次に、マージはローカルで繰り返すことによって再生されるか、別のノードから結果パーツを取得することによって行われます。正確な動作は構成可能で、CPUの消費とネットワークI/Oのバランスを取ることができます。例えば、クロスデータセンターレプリケーションは通常、運用コストを最小限に抑えるためにローカルマージを好みます。3つ目に、ノードは互いに独立したレプリケーションログエントリーを並行して再生します。これにより、例えば、同じテーブルに連続して挿入された新しいパーツの取得や、異なるテーブルに対する操作が含まれます。
3.7 ACID準拠
同時読み取りと書き込み操作のパフォーマンスを最大化するために、ClickHouseはロックをできるだけ避けます。クエリは、クエリの開始時に作成されたすべての関与するテーブルのすべてのパーツのスナップショットに対して実行されます。これにより、並行 INSERT やマージによって挿入された新しいパーツ(セクション 3.1) が実行に参加しないことが保証されます。パーツが同時に変更または削除されるのを防ぐために(セクション 3.4)、処理されたパーツの参照カウントはクエリの実行中に増加します。形式的には、これはバージョン管理されたパーツに基づくMVCCバリアントによって実現されたスナップショット隔離に対応します [6]。その結果、ステートメントは一般にACID準拠ではなく、スナップショットが取得された時点で行われる同時書き込みが単一のパーツにのみ影響する稀なケースを除きます。
実際には、ClickHouseの書き込みが多い意思決定のユースケースのほとんどは、停電の際に新しいデータを失うリスクを小さく許容します。データベースは、デフォルトで新しく挿入されたパーツをディスクにコミット(fsync)することを強制せず、カーネルに書き込みをバッチ処理させ、原子性を犠牲にすることによってこれを活用します。
4 クエリ処理層

図7: SIMDユニット、コア、およびノード間での並列化。
図7によって示されているように、ClickHouseはデータ要素、データチャンク、およびテーブルシャードのレベルでクエリを並列化します。複数のデータ要素は、SIMD命令を使用してオペレーター内で同時に処理できます。単一ノード上では、クエリエンジンが複数のスレッドでこれらのオペレーターを同時に実行します。ClickHouseは、MonetDB/X100 [11] と同じベクトル化モデルを使用しており、即ちオペレーターは単一の行ではなく複数の行(データチャンク)を生成、渡し、消費して、仮想関数呼び出しのオーバーヘッドを最小限に抑えます。ソーステーブルが分離されたテーブルシャードに分割されている場合、複数のノードがシャードを同時にスキャンできます。その結果、すべてのハードウェアリソースが完全に活用され、ノードを追加することによって水平に、コアを追加することによって垂直にクエリ処理をスケールできます。
このセクションの残りでは、まずデータ要素、データチャンク、およびシャードの粒度での並列処理をより詳細に説明します。次に、クエリパフォーマンスを最大化するための主要な最適化をいくつか紹介します。最後に、ClickHouseが同時クエリの存在下で共有システムリソースをどのように管理するかについて説明します。
4.1 SIMD並列化
オペレーター間で複数行を渡すことは、ベクトル化の機会を生み出します。ベクトル化は、手動で書かれたインストリンシック [64, 80] またはコンパイラの自動ベクトル化 [25] に基づいています。ベクトル化の恩恵を受けるコードは異なるコンピュートカーネルにコンパイルされます。例えば、クエリオペレーターの内部ホットループは、非ベクトル化カーネル、自動ベクトル化AVX2カーネル、手動でベクトル化されたAVX-512カーネルに基づいて実装することができます。最も速いカーネルは、cpuid
命令に基づいてランタイムで選ばれます。このアプローチにより、ClickHouseは15年前の古いシステムでも動作可能(その最低要件はSSE 4.2)でありながら、最近のハードウェア上での著しい速度向上を提供します。
4.2 マルチコア並列化

図8: 三つのレーンを持つ物理オペレーター計画。
ClickHouseは、SQLクエリを物理計画オペレーターの指向グラフに変換する従来のアプローチ [31] に従っています。オペレーター計画の入力は、ネイティブまたはサポートされているいずれかの外部形式からデータを読み取る特別なソースオペレーターによって表されます(セクション 5) 参照)。同様に、特別なシンクオペレーターは結果を希望の出力形式に変換します。物理オペレーター計画は、クエリコンパイル時に、構成可能な最大ワーカースレッド数(デフォルトではコアの数)とソーステーブルのサイズに基づいて、独立した実行レーンに展開されます。レーンは、並列オペレーターによって処理されるデータを重複しない範囲に分解します。並列処理の機会を最大化するために、レーンはできるだけ遅くマージされます。
例として、図8のノード1のボックスは、ページインプレッション統計テーブルに対する典型的なOLAPクエリのオペレーターグラフを示しています。最初のステージでは、ソーステーブルの3つの不連続範囲が同時にフィルタリングされます。リパーティション交換オペレーターは、処理スレッドの均一な利用を維持するために、結果チャンクを第一および第二のステージの間で動的にルーティングします。スキャンされた範囲が著しく異なる選好性を持っている場合、第一ステージ後にレーンが不均衡になる可能性があります。第二ステージでは、フィルターを生き残った行がRegionIDごとにグループ化されます。アグリゲートオペレーターは、RegionIDをグループカラムとして、各グループの合計とカウントを部分的な集計状態として維持します。ローカル集計結果は最終的にグローバル集計結果に向けてGroupStateMergeオペレーターによってマージされます。このオペレーターはパイプラインブレイカーでもあり、集計結果が完全に計算されるまで第三のステージは開始できません。第三のステージでは、結果グループはまずリパーティション交換オペレーターによって3つの等しい不連続パーティションに分割され、その後AvgLatencyでソートされます。ソートは3つのステップで実行されます。最初に、ChunkSortオペレーターが各パーティションの個々のチャンクをソートします。次に、StreamSortオペレーターがローカルなソート済み結果を維持し、2-wayマージソートを使用して新しいソート済みチャンクと組み合わせます。最後に、MergeSortオペレーターがローカル結果をk-wayソートを使用して組み合わせ、最終結果を得ます。
オペレーターは状態マシンであり、入力ポートと出力ポートを介して互いに接続されています。オペレーターの3つの可能な状態はneed-chunk、ready、doneです。need-chunkからreadyに移動するには、チャンクがオペレーターの入力ポートに配置されます。readyからdoneに移動するには、オペレーターが入力チャンクを処理し、出力チャンクを生成します。doneからneed-chunkに移動するには、出力チャンクがオペレーターの出力ポートから削除されます。2つの接続されたオペレーターにおける最初と第三の状態遷移は、結合ステップでのみ行われます。ソースオペレーター(シンクオペレーター)は、状態としてreadyとdone(need-chunkとdone)しか持ちません。
ワーカースレッドは物理オペレーター計画を継続的に移動し、状態遷移を実行します。CPUキャッシュをホットに保つために、計画には同じスレッドが同じレーン内の連続したオペレーターを処理すべきであるというヒントが含まれています。並列処理は、ステージ内の重複しない入力間で水平方向に行われ(例:図8 ではアグリゲートオペレーターが同時に実行)、パイプラインブレイカーにより分離されないステージを通じて垂直方向にも行われます(例:図8 では同じレーン内のフィルターとアグリゲートオペレーターが同時に実行できます)。新しいクエリが開始されたときや、同時クエリが終了したときに過剰または不足のサブスクリプションを避けるために、並列度はクエリ開始時に指定されたワーカースレッドの最大数との間で変更可能です(セクション 4.5) 参照)。
オペレーターはさらに、ランタイムでクエリの実行に影響を与える2つの方法でも動作します。第一に、オペレーターは新しいオペレーターを動的に作成し接続することができます。これは、メモリ消費が構成可能な閾値を超えた場合にクエリをキャンセルするのではなく、外部の集計、ソート、または結合アルゴリズムに切り替えるために主に使われます。第二に、オペレーターはワーカースレッドに非同期キューに移動するようリクエストすることができます。これにより、リモートデータを待機する際にワーカースレッドをより効果的に活用できます。
ClickHouseのクエリ実行エンジンとモーセルトリブン並列性 [44] は、通常、レーンが異なるコア / NUMAソケットで実行され、ワーカースレッドが他のレーンからタスクを盗むことができるという点で類似しています。さらに、中央のスケジューリングコンポーネントは存在せず、ワーカースレッドは物理オペレーター計画を継続的に走査することによって個別にタスクを選択します。モーセルトリブン並列性とは異なり、ClickHouseは最大の並列度を計画に組み込み、デフォルトのモーセルサイズ(約100,000行)と比較してソーステーブルをパーティション分割するためにはるかに大きな範囲を使用します。この場合、一部ではスタールが発生する可能性があります(例:異なるレーンのフィルターオペレーターのランタイムが大きく異なる場合)が、リパーティションなどの交換オペレーターを自由に使用することで、少なくともそのような不均衡がステージ間で蓄積することを避けることができます。
4.3 マルチノード並列化
クエリのソーステーブルがシャーディングされている場合、クエリを受け取ったノード(イニシエータノード)のクエリオプティマイザは、他のノードでできるだけ多くの作業を実行しようとします。他のノードからの結果は、クエリ計画の異なるポイントに統合できます。クエリに応じて、リモートノードは以下のいずれかを実行します:1. 生のソーステーブルカラムをイニシエータノードにストリーム、2. ソースカラムをフィルタリングし、生き残った行を送信、3. フィルタリングと集計ステップを実行し、ローカル結果グループを部分的な集計状態と共に送信、または4. フィルター、集計、およびソートを含む全クエリを実行。
図8のノード2 ... Nは、ヒットテーブルのシャードを保持している他のノードで実行される計画フラグメントを示しています。これらのノードはローカルデータをフィルタリングし、グループ化し、結果をイニシエータノードに送信します。ノード1のGroupStateMergeオペレーターは、ローカルおよびリモート結果をマージしてから、結果グループを最終的にソートします。
4.4 全体的パフォーマンス最適化
このセクションでは、クエリ実行の異なるステージに適用される選択された主要なパフォーマンス最適化を示します。
クエリ最適化。最初の最適化セットは、クエリのASTから取得された意味論的クエリ表現の上に適用されます。このような最適化の例には、定数フォールディング(例:concat(lower('a'),upper('b')) は 'aB' になる)、特定の集計関数からスカラーの抽出(例:sum(a2) は 2 * sum(a) になる)、共通部分式の排除、および等価フィルターの論理和をINリストに変換すること(例:x=c OR x=d は x IN (c,d) になる)があります。最適化された意味論的クエリ表現は、論理オペレーター計画に変換されます。論理計画の上に適用される最適化には、フィルタープッシュダウン、関数評価とソートステップの順序の再配置が含まれますが、よりコストが高いと推定されるものに基づきます。最終的に、論理クエリ計画は物理オペレーター計画に変換されます。この変換は、関与するテーブルエンジンの特性を利用することができます。例えば、MergeTree-テーブルエンジンの場合、ORDER BYカラムが主キーのプレフィックスを形成するなら、データをディスク順に読み取ることができ、ソートオペレーターを計画から除去することができます。また、集約でのグループ化カラムが主キーのプレフィックスを形成する場合、ClickHouseはソート集約 [33] を使用できます。すなわち、事前にソートされた入力中で同じ値の集約実行を直接行います。ハッシュ集約と比較して、ソート集約は記憶容量を大幅に軽減し、集約値は実行が完了した後すぐに次のオペレーターに渡されます。
クエリコンパイル。ClickHouseはLLVMに基づくクエリコンパイルを採用し、隣接する計画オペレーターを動的に融合します [38, 53]。例えば、式a * b + c + 1は、三つのオペレーターではなく単一のオペレーターに結合できます。式だけでなく、ClickHouseは同時に複数の集計関数を評価するため(つまり、GROUP BYに対して)のコンパイルや、二つ以上のソートキーでのソートにもコンパイルを使用します。クエリコンパイルは、仮想呼び出しの数を減少させ、データをレジスタやCPUキャッシュに保持し、コードが必要のない分岐予測を助けます。さらに、ランタイムコンパイルにより、論理最適化やコンパイラに実装されたピー プホール最適化の豊富なセットが可能になり、利用可能な最速のローカルCPU命令へのアクセスも可能になります。コンパイルは、同じ通常、集約、またはソート式が構成可能な回数よりも多く異なるクエリで実行されるときだけ開始されます。コンパイルされたクエリオペレーターはキャッシュされ、将来のクエリに再利用することができます。[7]
主キーインデックス評価。ClickHouseは、条件の論理積正規形においてフィルタークローズのサブセットが主キーのカラムのプレフィックスを構成する場合、主キーインデックスを使用してWHERE条件を評価します。主キーインデックスは、キー値が辞書式にソートされた範囲に対して、左から右に分析されます。主キーのカラムに対応するフィルター条件は三値論理を使用して評価されます-すべて真、すべて偽、または範囲内の値に対して混合の真偽です。後者の場合、範囲は再帰的に分析するためにサブ範囲に分割されます。フィルター条件における関数のための追加最適化があります。まず、関数はその単調性を記述する特性を持っており、例えば、toDayOfMonth(date)は月内で部分的に単調です。単調性特性により、ソートされた入力キー値範囲に対して、関数がソートされた結果を生成するかどうかを推測することができます。第二に、いくつかの関数は、与えられた関数結果の原像を計算することができます。これは、定数の比較を主キーのカラムに対する関数呼び出しと置き換えるために、キーコラムの値を原像と比較するために使用されます。例えば、toYear(k) = 2024は k >= 2024-01-01 && k < 2025-01-01 に置き換えることができます。
データスキッピング。ClickHouseは、セクション 3.2. で紹介したデータ構造を使用して、クエリ実行時のデータ読み込みを回避しようとします。さらに、異なるカラムのフィルターは、推定選好性の降順の順に従って、ヒューリスティックスおよび(オプションの)カラム統計に基づいて逐次評価されます。1つ以上の一致する行が含まれるデータチャンクのみが次の述部に渡されます。これにより、読み取るデータ量と各述部から実行される計算量が徐々に減少します。この最適化は、少なくとも1つの高選好の述部が存在する場合にのみ適用されます。そうでない場合、すべての述部を並行して評価するよりもクエリのレイテンシが悪化するからです。
ハッシュテーブル。ハッシュテーブルは、集計とハッシュ結合のための基本的なデータ構造です。適切なタイプのハッシュテーブルを選択することはパフォーマンスにとって重要です。ClickHouseはハッシュテーブルを多様にインスタンス化します(2024年3月時点で30以上)。これは、ハッシュ関数、アロケーター、セルタイプ、リサイズポリシーを変数ポイントとして持つ一般的なハッシュテーブルテンプレートから生成されます。グループ化カラムのデータ型、推定されるハッシュテーブルのカーディナリティ、その他の要因に応じて、各クエリオペレーターのために最速のハッシュテーブルが選択されます。ハッシュテーブルに対して実装されたさらなる最適化には、次のようなものがあります:
- 256のサブテーブルを持つ二重レイアウト(ハッシュの最初のバイトに基づく)として、巨大なキーセットをサポート、
- 文字列ハッシュテーブル [79] が文字列長ごとに異なるハッシュ関数を持つ4つのサブテーブルを持つ、
- キーが少数の場合、バケットインデックスとしてキーを直接使用するルックアップテーブル(すなわちハッシュなし)、
- 比較が高コストである際に衝突解決を高速化するための埋め込まれたハッシュを持つ値(例えば、文字列、AST)、
- ランタイムの統計から予測されるサイズに基づいてハッシュテーブルを作成し、不要なリサイズを避けること、
- 単一メモリスラブ内で作成・破棄ライフサイクルが同じ多数の小規模ハッシュテーブルの割り当て、
- ハッシュテーブルの再利用のために、パーハッシュマップおよびパーセルバージョンカウンタによる即時クリアリング、
- ハッシュしたキーの値を取得する際のスピードアップのためのCPUプリフェッチ(__builtin_prefetch)の使用。
結合。ClickHouseは当初結合を初歩的にしかサポートしていなかったため、多くのユースケースは歴史的に非正規化テーブルに頼っていました。今日、データベースはSQLで利用可能なすべての結合タイプ(内側、左/右/完全外側、クロス、一致時)を提供し、ハッシュ結合(ナイーブ、グレース)、ソートマージ結合、迅速なキー-バリュー検索のためのインデックス結合などの異なる結合アルゴリズムも提供しています。
結合はデータベース操作の中で最もコストがかかるため、古典的な結合アルゴリズムの並列バリアントを提供することが重要です。理想的には、調整可能なスペース/時間のトレードオフを持つことです。ClickHouseは、[7] からの非ブロッキング、共有パーティションアルゴリズムをハッシュ結合に実装しています。例えば、図9 のクエリは、ページヒット統計テーブルでURL間のユーザー移動を自己結合を用いて計算します。結合のビルドフェーズは、ソーステーブルの3つの不連続範囲をカバーする3つのレーンに分割されます。グローバルハッシュテーブルの代わりに、パーティション化されたハッシュテーブルが使用されます。(通常3つの)ワーカースレッドは、ハッシュ関数のモジュロを計算することによってビルド側の各入力行のターゲットパーティションを決定します。ハッシュテーブルのパーティションへのアクセスは、Gather交換オペレーターを使用して同期されます。プローブフェーズは、入力タプルのターゲットパーティションを同様に見つけます。このアルゴリズムは、各タプルごとに2つの追加のハッシュ計算を導入しますが、ビルドフェーズにおけるロックの衝突を大幅に削減します。これは、ハッシュテーブルのパーティション数に依存します。

図9: 三つのハッシュテーブルパーティションを持つ並列ハッシュ結合。
4.5 ワークロード隔離
ClickHouseは並行制御、メモリ使用制限、およびI/Oスケジューリングを提供し、ユーザーがクエリをワークロードクラスに隔離できるようにします。特定のワークロードクラスに対して共有リソース(CPUコア、DRAM、ディスクおよびネットワークのI/O)に制限を設定することによって、これらのクエリが他の重要なビジネスクエリに影響を及ぼさないようにします。
並行制御は、高い数の同時クエリがあるシナリオでスレッドの過剰サブスクリプションを防ぎます。より具体的には、クエリごとのワーカースレッドの数は、利用可能なCPUコア数に対する指定された比率に基づいて動的に調整されます。
ClickHouseは、サーバー、ユーザー、およびクエリレベルでのメモリアロケーションのバイトサイズを追跡し、それによって柔軟なメモリ使用制限を設定することを可能にします。メモリオーバーコミットにより、他のクエリに対するメモリ制限を保証しながら、保証されたメモリを超える追加の未使用メモリをクエリが使用できるようになります。さらに、集計、ソート、結合句に対するメモリ使用量を制限することができ、メモリ制限を超えた場合に外部アルゴリズムへのフォールバックが発生します。
最後に、I/Oスケジューリングにより、ユーザーはワークロードクラスのためにローカルおよびリモートディスクアクセスを最大帯域幅、リクエストの混在、ポリシー(例:FIFO、SFC [32])に基づいて制限できます。
5 統合層
リアルタイムの意思決定アプリケーションはしばしば、複数の場所におけるデータへの効率的で低レイテンシアクセスを必要とします。外部データをOLAPデータベースで利用可能にするためには2つのアプローチがあります。プッシュベースのデータアクセスでは、サードパーティコンポーネントがデータベースと外部データストアを橋渡しします。これに対するひとつの例は、リモートデータを目的のシステムにプッシュする特殊な抽出-変換-ロード(ETL)ツールです。プルベースモデルでは、データベース自体がリモートデータソースに接続し、クエリ用にローカルテーブルにデータをプルダウンするか、リモートシステムにデータをエクスポートします。プッシュベースのアプローチはより柔軟性がありますが、アーキテクチャのフットプリントが大きく、スケーラビリティのボトルネックを伴います。それに対して、データベース内のリモート接続は、ローカルデータとリモートデータ間の結合などの興味深い機能を提供し、全体のアーキテクチャをシンプルに保ちながら洞察への時間を短縮します。
このセクションの残りでは、リモートの場所にあるデータにアクセスすることを目的としたClickHouseのプルベースのデータ統合方法を探索します。SQLデータベースにおけるリモート接続のアイデアは新しくありません。例えば、SQL/MED標準 [35] は、2001年に導入され、2011年からPostgreSQLによって実装されていますが、外部データを管理するための統一インターフェースとして外国データラッパーを提案しています。他のデータストアおよびストレージフォーマットとの最大の相互運用性は、ClickHouseの設計目標の1つです。2024年3月時点で、ClickHouseは、すべての分析データベースにおいて、私たちの知識の範囲内で最も組み込まれたデータ統合オプションを提供します。
外部接続。ClickHouseは、外部システムやストレージ場所との接続のために、ODBC、MySQL、PostgreSQL、SQLite、Kafka、Hive、MongoDB、Redis、S3/GCP/Azureオブジェクトストアおよびさまざまなデータレイクを含む50以上の 統合テーブル関数とエンジンを提供します。これらは次のボーナス図によってさらに分類されます(元のVLDB論文には含まれていません)。

ボーナス図: ClickBenchの相互運用性オプション。
統合テーブル関数による一時的なアクセス。テーブル関数は、SELECTクエリのFROM句で呼び出して、探索的アドホッククエリのためにリモートデータを読み取ることができます。あるいは、INSERT INTO TABLE FUNCTIONステートメントを使用して、リモートストアへの書き込みに使用することもできます。
保持されたアクセス。リモートデータストアや処理システムとの永続的な接続を作成するためには、三つの方法があります。
第一に、統合テーブルエンジンは、MySQLテーブルのようなリモートデータソースを永続的なローカルテーブルとして表現します。ユーザーはCREATE TABLE AS構文を使用してテーブル定義を保存し、SELECTクエリとテーブル関数と組み合わせます。リモートカラムのサブセットのみを参照するカスタムスキーマを指定することや、カラム名と同等のClickHouseタイプを自動的に決定するためにスキーマ推論を使用することも可能です。また、受動的および能動的なランタイム動作を区別します。受動的テーブルエンジンは、クエリをリモートシステムに転送し、結果でローカルプロキシテーブルを埋めます。この対照的に、能動的テーブルエンジンは、リモートシステムから定期的にデータをプルするか、リモート変更にサブスクライブします。例えば、PostgreSQLの論理レプリケーションプロトコルを通じて取得されます。その結果、ローカルテーブルにはリモートテーブルの完全なコピーが含まれます。
第二に、統合データベースエンジンは、リモートデータストア内のテーブルスキーマのすべてのテーブルをClickHouseにマッピングします。前者とは異なり、一般にリモートデータストアがリレーショナルデータベースであることが要求され、DDLステートメントに対して限定的なサポートを提供します。
第三に、ディクショナリは、対応する統合テーブル関数またはエンジンを用いて、ほぼすべての可能なデータソースに対する任意のクエリを使用して埋め込むことができます。ランタイム動作は能動的であり、データはリモートストレージから定常的な間隔でプルされます。
データフォーマット。3rdパーティシステムと対話するために、現代の分析データベースは、あらゆる形式のデータを処理できる必要があります。ClickHouseは、ネイティブ形式のほかに、90以上の形式をサポートしています。これにより、CSV、JSON、Parquet、Avro、ORC、Arrow、Protobufなどが含まれます。各形式は、ClickHouseが読み取ることができる入力形式であるか(ClickHouseがエクスポートできる)、またはその両方であることができます。Parquetのような分析志向の形式のいくつかは、クエリ処理と統合されており、最適化器が埋め込まれた統計を活用でき、圧縮データ上でフィルターを直接評価できます。
互換性インターフェース。ネイティブバイナリワイヤプロトコルとHTTPのほか、クライアントはMySQLまたはPostgreSQLワイヤプロトコル互換のインターフェースを介してClickHouseと対話できます。この互換性機能は、ベンダーがまだネイティブなClickHouse接続を実装していない独自のアプリケーション(例:特定のビジネスインテリジェンスツール)からのアクセスを有効にするために便利です。
6 パフォーマンスを特徴とする
このセクションでは、パフォーマンス分析のためのビルトインツールを紹介し、実際のクエリおよびベンチマーククエリを使用してパフォーマンスを評価します。
6.1 ビルトインパフォーマンス分析ツール
個々のクエリやバックグラウンド操作におけるパフォーマンスボトルネックを調査するために、多様なツールが利用可能です。ユーザーは、システムテーブルに基づく統一インターフェースを介してすべてのツールと相互作用します。
サーバーおよびクエリメトリック。アクティブパート数、ネットワークスループット、キャッシュヒット率などのサーバーレベルの統計は、読み取られたブロックの数やインデックス使用統計などのクエリごとの統計によって補完されます。メトリックは、リクエスト時に同期的(任意に)または構成可能な間隔で非同期的に計算されます。
サンプリングプロファイラー。サーバースレッドのコールスタックは、サンプリングプロファイラーを使用して収集できます。その結果は、オプションでフレームグラフビジュアライザーなどの外部ツールにエクスポートできます。
OpenTelemetry統合。OpenTelemetryは、複数のデータ処理システム間でデータ行をトレースするためのオープンスタンダードです [8]。ClickHouseは、すべてのクエリ処理ステップに対して構成可能な粒度でOpenTelemetryログスパンを生成し、他のシステムからOpenTelemetryログスパンを収集・分析できます。
クエリの説明。他のデータベースと同様に、SELECTクエリは、クエリのAST、論理および物理オペレーター計画、実行時の動作に関する詳細な洞察のためにEXPLAINによって前置されることができます。
6.2 ベンチマーク
ベンチマークは、現実的でないという批判を受けている [10, 52, 66, 74]ものの、データベースの強みや弱みを特定するためには依然有用です。以下では、ClickHouseのパフォーマンスを評価するためにベンチマークがいかに使用されるかを議論します。