可観測性のためのスキーマ設計
ユーザーには、以下の理由からログとトレース用の独自のスキーマを常に作成することをお勧めします。
- 主キーの選択 - デフォルトのスキーマは特定のアクセスパターンに最適化された
ORDER BY
を使用します。あなたのアクセスパターンがこれと一致する可能性は低いです。 - 構造の抽出 - ユーザーは既存のカラムから新しいカラムを抽出したい場合があります。例えば、
Body
カラムのように。この作業は、マテリアライズドカラム(そして、より複雑な場合にはマテリアライズドビュー)を使用して行うことができます。これにはスキーマの変更が必要です。 - マップの最適化 - デフォルトのスキーマは属性の格納にマップ型を使用します。これらのカラムは任意のメタデータの格納を可能にします。この機能は必須ですが、イベントからのメタデータが事前に定義されていないことが多く、したがって ClickHouse のような強い型のデータベースでは他に格納できないため、マップのキーとその値へのアクセスは通常のカラムへのアクセスほど効率的ではありません。私たちは、スキーマを変更し、最も一般的にアクセスされるマップキーをトップレベルのカラムにすることによってこれに対処します - 詳細は "SQLによる構造の抽出" を参照してください。これにはスキーマの変更が必要です。
- マップキーアクセスの簡略化 - マップ内のキーにアクセスするには、より冗長な構文が必要です。ユーザーはエイリアスを使用してこれを軽減できます。クエリを簡素化するために、"エイリアスの使用" を参照してください。
- セカンダリインデックス - デフォルトのスキーマは、マップへのアクセスを高速化し、テキストクエリを加速するためにセカンダリインデックスを使用します。これらは通常必要ではなく、追加のディスクスペースを消費します。これらは使用できますが、必要であることを確認するためにテストされるべきです。詳細は "セカンダリ/データスキッピングインデックス" を参照してください。
- コーデックの使用 - ユーザーは、想定されるデータを理解しており、これが圧縮の改善につながるという証拠がある場合は、カラムのコーデックをカスタマイズしたいと考えるかもしれません。
上記の各ユースケースについて詳細に説明します。
重要: ユーザーは最適な圧縮とクエリパフォーマンスを達成するために、自身のスキーマを拡張および変更することを奨励されていますが、可能な限り基本カラムに対して OTel スキーマの命名規則に従うべきです。ClickHouse Grafana プラグインは、クエリ構築を支援するために基本的な OTel カラム(例: Timestamp や SeverityText)の存在を前提としています。ログとトレースに必要なカラムは、こちらに文書化されています [1][2] および こちら において、それぞれ確認できます。これらのカラム名を変更することを選択し、プラグイン設定でデフォルトを上書きすることができます。
SQLによる構造の抽出
構造化されたログまたは非構造化されたログを取り込む際、ユーザーは次の機能を必要とすることがよくあります。
- 文字列のブロブからカラムを抽出する。これにクエリする際、文字列操作を使用するよりも速くなります。
- マップからキーを抽出する。デフォルトのスキーマは、任意の属性をマップ型のカラムに配置します。この型は、ログとトレースを定義する際に属性のカラムを事前に定義する必要がないという利点があるスキーマレスの能力を提供します。このことは、Kubernetes からログを収集し、ポッドラベルを後で検索できるように保持したい場合にしばしば不可能です。マップキーとその値へのアクセスは、通常の ClickHouse カラムでのクエリよりも遅くなるため、マップからキーをルートテーブルのカラムに抽出することが望まれることが多いです。
次のクエリを考えてみてください:
構造化ログを使用して、どの URL パスが最も多くの POST リクエストを受け取るかをカウントしたいとします。JSONのブロブは、Body
カラムに文字列として格納されています。さらに、もしユーザーがコレクターで json_parser を有効にした場合は、LogAttributes
カラムにも Map(String, String)
として格納されている可能性があります。
LogAttributes
が利用可能であると仮定すると、サイトの URL パスが最も多くの POST リクエストを受け取るかをカウントするためのクエリは次のようになります:
ここでのマップ構文の使用に注意してください。例えば、LogAttributes['request_path']
や、URL からクエリパラメータを取り除くための path
関数 です。
もしユーザーがコレクターで JSON パースを有効にしていない場合、LogAttributes
は空になり、文字列 Body
からカラムを抽出するために JSON 関数 を使用する必要があります。
一般的に、構造化ログの JSON パースは ClickHouse で行うことをお勧めします。ClickHouse が最も高速な JSON パース実装であることに自信を持っています。しかし、ユーザーが他のソースにログを送信したい場合や、このロジックを SQL に居住させたくない場合もあることを認識しています。
今度は非構造化ログについて考えてみましょう:
非構造化ログの同様のクエリでは、extractAllGroupsVertical
関数 を使用して正規表現が必要です。
非構造化ログをパースするためのクエリの複雑さとコストが増加する(パフォーマンスの違いに注意)ため、可能な限り構造化ログを使用することをお勧めします。
上記のクエリは、正規表現辞書を活用するように最適化できます。詳細は 辞書の使用 を参照してください。
上記の2つのユースケースは、挿入時に上記のクエリロジックを移動することで ClickHouse によって満たされます。以下で、各アプローチが適切な状況を強調します。
ユーザーは、OTel コレクターのプロセッサやオペレーターを使用して処理を実行することもできます。詳細は こちら を参照してください。ほとんどの場合、ClickHouse はコレクターのプロセッサよりもはるかにリソース効率が良く、迅速であることがわかります。すべてのイベント処理を SQL で行うことの主な欠点は、ソリューションが ClickHouse に結びつくことです。たとえば、ユーザーが処理されたログを OTel コレクターから別の宛先(例:S3)に送信したいと考えることがあります。
マテリアライズドカラム
マテリアライズドカラムは、他のカラムから構造を抽出する最も簡単なソリューションを提供します。このようなカラムの値は常に挿入時に計算され、INSERT クエリで指定することはできません。
マテリアライズドカラムは、挿入時に新しいカラムとしてディスクに抽出されるため、追加のストレージオーバーヘッドが発生します。
マテリアライズドカラムは任意の ClickHouse 表現をサポートし、文字列の処理(正規表現や検索を含む)や URL を処理するための任意の分析関数を活用することができます。また、型変換、JSON からの値の抽出、または 数学的操作を実行できます。
基本的な処理にはマテリアライズドカラムをお勧めします。特に、マップから値を抽出し、それらをルートカラムに昇格させ、型変換を実行するのに便利です。これらは、非常に基本的なスキーマやマテリアライズドビューと組み合わせて使用されると最も便利です。次のスキーマは、コレクターによって JSON が LogAttributes
カラムに抽出されたログ用です:
JSON 関数を使用して抽出するための同等のスキーマは こちら で確認できます。
マテリアライズドビューの 3 つのカラムは、リクエストページ、リクエストタイプ、リファラーのドメインを抽出します。これらはマップキーにアクセスし、それらの値に関数を適用します。その後のクエリは大幅に高速化されます:
マテリアライズドカラムは、デフォルトでは SELECT *
で返されません。これは、SELECT *
の結果が常に INSERT を使用してテーブルに再挿入できることを保証するためです。この動作は、 asterisk_include_materialized_columns=1
を設定することで無効にでき、Grafana でも有効にできます(データソース設定の 追加設定 -> カスタム設定
を参照)。
マテリアライズドビュー
マテリアライズドビューは、ログやトレースに対して SQL フィルタリングと変換を適用するためのより強力な手段を提供します。
マテリアライズドビューでは、計算コストをクエリ時から挿入時にシフトすることができます。ClickHouse のマテリアライズドビューは、データがテーブルに挿入されるときにバッチに対してクエリを実行するトリガーです。このクエリの結果が第2の「ターゲット」テーブルに挿入されます。

ClickHouse のマテリアライズドビューは、基づくテーブルにデータが流入するとリアルタイムで更新され、継続的に更新されるインデックスのように機能します。対照的に、他のデータベースでは、マテリアライズドビューは通常、リフレッシュが必要な静的なクエリのスナップショットです(ClickHouse の更新可能なマテリアライズドビューに似ています)。
関連するマテリアライズドビューのクエリは、理論上は任意のクエリを使用できますが、集約を含むこともできます。ただし、ジョインには制限があります。ログとトレースの処理およびフィルタリングのワークロードには、ユーザーは任意の SELECT
ステートメントを使用できると考えても問題ありません。
ユーザーは、クエリは挿入される行を対象とするテーブル(ソーステーブル)で実行され、その結果が新しいテーブル(ターゲットテーブル)に送信されるトリガーであることを忘れないでください。
データ를2重に保存しないようにするために(ソーステーブルとターゲットテーブルの両方)、ソーステーブルのテーブルに Null テーブルエンジン を変更し、元のスキーマを保持できます。我们的 OTel 收集器将继续向此表发送数据。例如,对于日志,otel_logs
テーブルは以下のようになります:
Null テーブルエンジンは強力な最適化です - /dev/null
と考えてください。このテーブルはデータを保存しませんが、結合されたマテリアライズドビューは挿入された行に対して仍然 実行されます。
次のクエリでは、私たちが保存したい形式に行を変換し、LogAttributes
からすべてのカラムを抽出します(これはコレクターによって json_parser
オペレーターを使用して設定されたと仮定しています)、SeverityText
と SeverityNumber
(特定の条件に基づいて設定)を設定します。この場合、私たちは私たちが知っているカラムのみを選択します - TraceId
、SpanId
、TraceFlags
などのカラムを無視します。
上記では Body
カラムも抽出しています - 追加の属性が後で追加される場合のためです。ClickHouse ではこのカラムは圧縮されやすく、あまりアクセスされないため、クエリパフォーマンスに悪影響を及ぼすことはありません。最後に、Timestamp を DateTime に削減します(スペースを節約するため - 詳細は "型の最適化" を参照)し、キャストを行います。
上記での 条件付き関数 の使用に注意してください。これらは複雑な条件を形成するのに非常に便利であり、マップ内の値が設定されているかどうかをチェックするために有用です - 私たちは『すべてのキーが LogAttributes
に存在するものと仮定する』と無邪気に考えています。ユーザーはこれらに精通することをお勧めします - これらはログのパースにおいて友人であり、null 値の処理関数に関する関数と同様です!
これらの結果を受け取るテーブルが必要です。以下のターゲットテーブルは、上記のクエリに一致します。
ここで選択された型は、"型の最適化"で議論された最適化に基づいています。
スキーマが大幅に変更されたことに注意してください。実際には、ユーザーは保持したいトレースカラムやカラム ResourceAttributes
を持つことが考えられます(これは通常 Kubernetes メタデータを含みます)。Grafana は、トレースカラムを活用してログとトレースの間のリンク機能を提供できます - 詳細は "Grafanaの使用" を参照してください。
以下で、マテリアライズドビュー otel_logs_mv
を作成します。このビューは、otel_logs
テーブルの上記の選択を実行し、結果を otel_logs_v2
に送信します。
上記のビューは、以下のように視覚化存在します:

次に、"ClickHouse へのエクスポート" で使用されるコレクター構成を再起動すると、希望する形式で otel_logs_v2
にデータが表示されます。型付きの JSON 抽出関数の使用に注意してください。
Body
カラムから JSON 関数を使用してカラムを抽出する同等のマテリアライズドビューは以下のように示されます。
型に注意
上記のマテリアライズドビューは、特に LogAttributes
マップを使用する場合に暗黙的なキャストに依存しています。ClickHouse は、抽出された値をターゲットテーブルの型に透明にキャストすることが多く、必要な構文が削減されます。ただし、ユーザーには、ターゲットテーブルと同じスキーマを持つ INSERT INTO
ステートメントを使用してビューをテストすることを常にお勧めします。これにより、型が正しく処理されていることを確認できます。特に以下のケースに注意を払うべきです:
- マップ内でキーが存在しない場合は、空の文字列が返されます。数値の場合、ユーザーはこれらを適切な値にマップする必要があります。これは、条件付き関数 で実現できます。例えば、
if(LogAttributes['status'] = ", 200, LogAttributes['status'])
や、デフォルト値が許容される場合は、キャスト関数 を使用できます。例えば、toUInt8OrDefault(LogAttributes['status'] )
です。 - 一部の型は常にキャストされない場合があります。例えば、数値の文字列表現は列挙型の値にキャストされません。
- JSON 抽出関数は、値が見つからない場合に、その型に対するデフォルト値を返します。これらの値が意味を持つことを確認してください!
主(順序)キーの選択
必要なカラムを抽出したら、順序/主キーの最適化を開始できます。
いくつかの単純なルールを適用して、順序キーを選択するのに役立ちます。以下では、時折競合する可能性があるため、これらを順番に考慮します。ユーザーは、このプロセスからキーを識別でき、通常は4〜5個で十分です:
- 一般的なフィルタリングとアクセスパターンに沿ったカラムを選択します。ユーザーが通常、特定のカラム(例:ポッド名)でフィルタリングして可観測性の調査を開始する場合、このカラムは
WHERE
句で頻繁に使用されます。頻繁に使用されるキーを含めることを優先してください。 - フィルタリング時に合計行の大半を除外するのに役立つカラムを優先し、読み取る必要のあるデータ量を減らします。サービス名やステータスコードは多くの場合良い候補です。後者の場合は、ユーザーが行のほとんどを除外する値でフィルタリングする場合のみにします。例えば、200 でフィルタリングすると、ほとんどのシステムでほとんどの行と一致しますが、500エラーは小さなサブセットに対応します。
- テーブル内の他のカラムと強く相関していると思われるカラムを優先します。これにより、これらの値が連続して格納され、圧縮が改善されます。
- 順序キーのカラムに対する
GROUP BY
およびORDER BY
操作は、メモリ効率を向上させることができます。
順序キーのサブセットを特定したら、特定の順序で宣言する必要があります。この順序は、クエリ内のセカンダリキー列のフィルタリング効率と、テーブルのデータファイルの圧縮率に大きな影響を及ぼす可能性があります。一般的には、カーディナリティの昇順でキーを並べるのが最良です。ただし、順序キーで後に表示されるカラムのフィルタリングは、組に表示される前のカラムよりも効率が悪くなることを考慮する必要があります。これらの動作のバランスを取り、アクセスパターンを考慮してください。最も重要なのは、バリエーションをテストすることです。順序キーについてのさらなる理解と最適化については、この記事を推奨します。
ログを構造化した後に順序キーを決定することをお勧めします。属性マップ内のキーや JSON 抽出式を順序キーとして使用しないでください。順序キーをテーブルのルートカラムとして持っていることを確認してください。
マップの使用
前の例では、Map(String, String)
カラム内の値にアクセスするためにマップ構文 map['key']
の使用が示されています。ネストされたキーにアクセスするためのマップの記法の使用に加えて、フィルタリングやこれらのカラムを選択するために使用できる特別な ClickHouse の マップ関数 があります。
例えば、以下のクエリは mapKeys
関数 を使用して LogAttributes
カラム内のすべてのユニークキーを識別し、次に groupArrayDistinctArray
関数(コンビネータ)を適用します。
マップカラム名にドットを使用しないことをお勧めします。使用する場合は _
を推奨します。
エイリアスの使用
マップ型のクエリは、通常のカラムよりも遅くなります - 参照してください "クエリの高速化"。さらに、構文がより複雑であり、ユーザーが記述するのが面倒になる場合があります。この後者の問題を解決するために、エイリアスカラムを使用することを推奨します。
ALIASカラムはクエリ時に計算され、テーブルには保存されません。したがって、このタイプのカラムに値をINSERTすることは不可能です。エイリアスを使用することで、マップキーを参照し、構文を簡素化し、マップエントリを通常のカラムとして透過的に公開できます。次の例を考えてみましょう:
いくつかのマテリアライズドカラムと、マップ LogAttributes
にアクセスする ALIAS
カラム RemoteAddr
があります。これにより、このカラムを介して LogAttributes['remote_addr']
の値をクエリできるため、クエリが簡素化されます。つまり、次のようになります。
さらに、ALTER TABLE
コマンドを通じて ALIAS
を追加するのは簡単です。これらのカラムは即座に使用可能です。例えば:
デフォルトでは、SELECT *
は ALIAS カラムを除外します。この動作は、asterisk_include_alias_columns=1
を設定することで無効にできます。
タイプの最適化
最適化タイプに関する 一般的なClickHouseのベストプラクティス は、ClickHouseの使用ケースに適用されます。
コーデックの使用
タイプの最適化に加えて、ユーザーはClickHouseの可観測性スキーマの圧縮を最適化する際に コーデックに関する一般的なベストプラクティス に従うことができます。
一般に、ユーザーは ZSTD
コーデックがログおよびトレースデータセットに非常に適用可能であることを見つけるでしょう。圧縮値をデフォルト値1から増加させると、圧縮が改善される可能性があります。ただし、これをテストする必要があります。高い値は、挿入時により多くのCPUオーバーヘッドをもたらすためです。通常、この値を増加させてもあまり利点は見られません。
さらに、タイムスタンプは圧縮に関してデルタエンコーディングから恩恵を受けますが、このカラムがプライマリ/オーダリングキーに使用されると、クエリパフォーマンスが遅くなることが示されています。ユーザーは、それぞれの圧縮とクエリパフォーマンスのトレードオフを評価することを推奨します。
辞書の使用
辞書は、さまざまな内部および外部 ソース からのデータのメモリ内 キー-バリュー 表現を提供するClickHouseの重要な機能です。これは、超低遅延の検索クエリ向けに最適化されています。

これは、取り込まれたデータを即座に豊かにし、処理プロセスを遅らせることなく、一般にクエリのパフォーマンスを向上させるさまざまなシナリオで役立ちます。JOINが特に恩恵を受けます。可観測性のユースケースではJOINがほとんど必要ないですが、エンリッチメントの目的で辞書は役立つことがあります - 挿入時およびクエリ時の両方でです。以下に両方の例を示します。
辞書を使用してJOINを加速することに興味があるユーザーは、こちらに詳細を見つけることができます。
挿入時とクエリ時
辞書は、クエリ時または挿入時にデータセットを豊かにするために使用できます。これらのアプローチにはそれぞれ利点と欠点があります。要約すると:
- 挿入時 - これは、エンリッチメント値が変更されず、辞書を同じデータに使用できる外部ソースに存在する場合に通常適切です。この場合、挿入時の行のエンリッチメントは、辞書へのクエリ時のルックアップを回避します。これにより、挿入のパフォーマンスが影響されるほか、強化された値がカラムとして保存されるため、追加のストレージオーバーヘッドが発生します。
- クエリ時 - 辞書内の値が頻繁に変わる場合、クエリ時のルックアップがより適用されることがよくあります。これにより、マップされた値が変わった場合にカラムを更新する必要がなく(およびデータを再書き込み)、フレキシブルさを得られます。ただし、このフレキシビリティは、クエリ時のルックアップコストの犠牲で得られます。このクエリ時のコストは、フィルター句で辞書のルックアップが必要な多くの行に対して特に顕著です。結果のエンリッチメント、つまり
SELECT
での場合、このオーバーヘッドは通常は目立ちません。
ユーザーは辞書の基本を理解することを推奨します。辞書は、専用の 特化関数 を使用して値を取得できるメモリ内のルックアップテーブルを提供します。
簡単なエンリッチメントの例については、こちらの辞書に関するガイドを参照してください。以下では、一般的な可観測性エンリッチメントタスクに焦点を当てます。
IP辞書の使用
ログとトレースを、IPアドレスを使用して緯度と経度の値で地理的にエンリッチすることは一般的な可観測性の要件です。これは ip_trie
構造化辞書を使用して達成できます。
公開されている DB-IP都市レベルのデータセット を、DB-IP.com の CC BY 4.0ライセンス の条件のもとで使用します。
README から、データは次のように構造化されていることがわかります:
この構造を考慮して、データを[url](/sql-reference/table-functions/url)テーブル関数を使用して覗いてみましょう:
簡単にするため、URL()
テーブルエンジンを使用して、フィールド名を持つClickHouseテーブルオブジェクトを作成し、行数を確認します:
ip_trie
辞書はIPアドレス範囲をCIDR記法で表す必要があるため、ip_range_start
と ip_range_end
を変換する必要があります。
各範囲のCIDRは、次のクエリで簡潔に計算できます:
上記のクエリには多くの処理があります。興味のある方は、非常に優れた説明を読んでください。それ以外の場合、上記はIP範囲のCIDRを計算します。
我々の目的のために必要なのはIP範囲、国コード、および座標だけですので、新しいテーブルを作成してGeo IPデータを挿入します:
低遅延のIPルックアップをClickHouseで実行するために、辞書を利用してGeo IPデータのキー -> 属性マッピングをメモリ内に保存します。ClickHouseは、ネットワークプレフィックス(CIDRブロック)を座標および国コードにマッピングするために ip_trie
辞書構造 を提供します。以下のクエリは、このレイアウトを使用して辞書を指定します。
辞書から行を選択して、このデータセットがルックアップ用に利用可能であることを確認します:
ClickHouseの辞書は、基になるテーブルデータと上記のライフタイム句に基づいて定期的に更新されます。DB-IPデータセットの最新の変更を反映させるために、geoip_urlリモートテーブルから geoip
テーブルにデータを再挿入するだけで辞書を更新できます。
Geo IPデータが ip_trie
辞書に読み込まれたので(便利に ip_trie
とも呼ばれます)、これを使用してIPの地理的位置を取得できます。これは、次のように dictGet()
関数 を使用して行うことができます。
ここでの取得速度に注意してください。これによりログをエンリッチできます。この場合、クエリ時のエンリッチメントを実施します。
元のログデータセットに戻り、上記を使用して国別にログを集計できます。以下は、RemoteAddress
カラムが抽出された以前のマテリアライズドビューから得られるスキーマを使用していることを前提としています。
IPアドレスから地理的な位置マッピングは変わる可能性があるため、ユーザーはリクエストが行われた時点でのリクエストの発生元を知りたいと思うでしょう - 同じアドレスの現在の地理的位置ではありません。この理由から、インデックスタイムのエンリッチメントが好まれることが多いです。これは、以下のようにマテリアライズドカラムを使用して行うことができます。
ユーザーは、新しいデータに基づいてIPエンリッチメント辞書を定期的に更新することを望むでしょう。これは、辞書のLIFETIME句を使用して実現でき、これにより辞書は基になるテーブルから定期的に再読み込まれます。基になるテーブルを更新する方法については、"リフレッシュ可能なマテリアライズドビュー"を参照してください。
上記の国および座標は、国別にグループ化およびフィルタリングする以上の視覚化能力を提供します。インスピレーションについては、"地理データの視覚化"を参照してください。
正規表現辞書の使用(ユーザーエージェントの解析)
ユーザーエージェント文字列の解析は古典的な正規表現の問題であり、ログとトレースベースのデータセットにおいて一般的な要件です。ClickHouseは、正規表現ツリー辞書を使用してユーザーエージェントを効率的に解析します。
正規表現ツリー辞書は、クリックハウスオープンソースでYAMLRegExpTree辞書ソースタイプを使用して定義され、正規表現ツリーを含むYAMLファイルへのパスが提供されます。独自の正規表現辞書を提供する場合は、必要な構造の詳細がこちらにあります。以下では、uap-coreを使用してユーザーエージェント解析を行い、サポートされるCSV形式の辞書を読み込みます。このアプローチはOSSおよびClickHouse Cloudと互換性があります。
次のメモリテーブルを作成します。これには、デバイス、ブラウザ、およびオペレーティングシステムの解析用の正規表現が保持されます。
次の公開ホストCSVファイルからこれらのテーブルを人口させることができます。urlテーブル関数を使用します:
メモリテーブルが充填されたので、正規表現辞書をロードできます。重要なのは、キー値をカラムとして指定する必要があることです - これらはユーザーエージェントから抽出できる属性になります。
これらの辞書が読み込まれたら、サンプルユーザーエージェントを提供し、新しい辞書抽出能力をテストします:
ユーザーエージェントに関するルールはあまり変わらないため、この抽出は挿入時に行うのが理にかなっています。
この作業をマテリアライズドカラムを使用して行うことも、マテリアライズドビューを使用することもできます。以前に使用されていたマテリアライズドビューを修正しましょう:
これにより、目的のテーブル otel_logs_v2
のスキーマを修正する必要があります:
収集器を再起動し、構造化されたログを取り込み、それに基づいて、新しく抽出されたDevice、Browser、Osカラムをクエリできます。
これらのユーザーエージェントカラムにタプルを使用する点に注意してください。タプルは、階層があらかじめわかっている複雑な構造に推奨されます。サブカラムは、マップキーとは異なり、通常のカラムと同じパフォーマンスを提供し、異種型を許可します。
さらなる読み込み
辞書に関するさらなる例や詳細については、以下の記事をお勧めいたします:
クエリの高速化
ClickHouseは、クエリパフォーマンスを加速するためのいくつかの技術をサポートしています。以下は、最も人気のあるアクセスパターンに最適化された適切なプライマリ/オーダリングキーを選択し、圧縮を最大化することを選択した後に考慮すべきです。これが通常、最小の労力でパフォーマンスに最大の影響を与えるでしょう。
集約に対するマテリアライズドビュー(増分)を使用する
前のセクションでは、データの変換とフィルタリングのためのマテリアライズドビューの使い方を探りました。しかし、マテリアライズドビューは、挿入時に集約をあらかじめ計算し、結果を保存するためにも使用できます。この結果は、後続の挿入からの結果で更新されるため、実質的に、挿入時に集約をあらかじめ計算できるようになります。
ここでの主な考え方は、結果が元のデータのより小さな表現(集約の場合は部分的なスケッチ)であることがよくあることです。結果をターゲットテーブルから読むための単純なクエリと組み合わせると、元のデータで同じ計算が実行されるよりもクエリ時間が速くなります。
次のクエリを考えてみましょう。構造化されたログを使用して時間あたりの総トラフィックを計算します:
これは、ユーザーがGrafanaで描くであろう一般的な折れ線グラフです。このクエリは非常に高速です - データセットはわずか1000万行であり、ClickHouseは速いです!しかし、これを数十億、数兆の行にスケールアップすると、理想的にはこのクエリパフォーマンスを維持したいと思うでしょう。
このクエリは、otel_logs_v2
テーブルを使用すれば10倍速くなります。このテーブルは以前のマテリアライズドビューからのものであり、 LogAttributes
マップからサイズキーを抽出します。ここでは説明の目的で生データを使用していますが、このクエリが一般的であれば以前のビューを使用することをお勧めします。
この処理をマテリアライズドビューを使用して挿入時に計算した結果を受け取るテーブルを作成する必要があります。このテーブルは、時刻ごとに1行のみを保持する必要があります。既存の時間に対して更新が受信された場合、他のカラムの内容は既存の時間の行にマージされる必要があります。この状態を増分でマージするためには、他のカラムの部分状態を保存しておく必要があります。
これには、ClickHouseの特別なエンジンタイプが必要です。SummingMergeTreeです。これは、同じオーダリングキーのすべての行を1つの行に置き換え、数値カラムの合計値を持つ行を作成します。以下のテーブルは、同じ日付の行をマージし、数値カラムの内容を合計します。
マテリアライズドビューをデモンストレーションするために、bytes_per_hour
テーブルが空でデータを受け取る前提として、今後のデータが otel_logs
から挿入され、上記の SELECT
が実行され、その結果が bytes_per_hour
に送られます。この構文は次のようになります:
ここでのTO
句が重要で、結果が送信される場所を示すものです。つまり、bytes_per_hour
です。
OTel Collectorを再起動し、ログを再送信すると、bytes_per_hour
テーブルは上記のクエリ結果で増分的にポピュレートされます。完了後、bytes_per_hour
のサイズを確認できます。時間ごとに1行ずつ保持されているはずです:
ここで、実際には otel_logs
では1000万行から113行に減少しています。重要なのは、新しいログが otel_logs
テーブルに挿入されると、bytes_per_hour
に対して新しい値が送信され、それぞれの時間ごとに非同期で自動的にマージされます。bytes_per_hour
は常に小さく最新の状態であります。
行のマージは非同期のため、クエリ時にユーザーがクエリを実行した場合、1時間に複数の行が存在する可能性があります。未処理の行をマージするために、次の2つのオプションがあります:
- (上記の
count
クエリに対して行ったように)テーブル名の末尾にFINAL
修飾子 を使用する。 - 最終テーブルで使用されるオーダリングキーで集約し、メトリックを合計します。
通常、2つ目のオプションはより効率的かつ柔軟です(このテーブルは他の目的にも使用できます)が、1つ目のオプションは一部のクエリで簡単に実行できます。両方を示します:
これにより、クエリの速度が0.6秒から0.008秒に向上し、75倍以上のスピードアップが達成されます!
これらのコスト削減は、より大きなデータセットでより複雑なクエリの場合はさらに大きくなります。例についてはこちらを参照してください。
インクリメンタル更新のために基数カウントを持続するには、AggregatingMergeTreeが必要です。
ClickHouseが集約状態が保存されることを理解できるように、UniqueUsers
カラムをAggregateFunction
型として定義し、部分状態の関数ソース(uniq)とソースカラムの型(IPv4)を指定します。SummingMergeTreeと同様に、同じORDER BY
キー値を持つ行はマージされます(上の例ではHour)。
関連するマテリアライズドビューは、前のクエリを使用します。
集約関数の末尾にState
というサフィックスを追加することに注意してください。これにより、関数の集約状態が最終結果の代わりに返されます。これには、他の状態とマージするための追加情報が含まれます。
データがコレクタの再起動を通じて再ロードされた後、unique_visitors_per_hour
テーブルに113行が利用可能であることを確認できます。
私たちの最終クエリは、関数に対してマージサフィックスを使用する必要があります(カラムが部分集約状態を保存しているため):
ここではFINAL
ではなくGROUP BY
を使用することに注意してください。
マテリアライズドビュー(インクリメンタル)を使用した迅速な検索
ユーザーは、フィルタや集約句で頻繁に使用されるカラムと共にClickHouseの並びキーを選択する際に、自分たちのアクセスパターンを考慮すべきです。これは、ユーザーが多様なアクセスパターンを持ち、それが単一のカラムセットに収束できない観測性のユースケースでは制限となる可能性があります。これは、デフォルトのOTelスキーマに組み込まれた例で最もよく示されます。トレースのデフォルトスキーマを考えてみましょう。
このスキーマは、ServiceName
、SpanName
、およびTimestamp
によるフィルタリングに最適化されています。トレースにおいて、ユーザーは特定のTraceId
による検索や関連するトレースのスパンを取得する能力も必要です。これは並びキーには存在しますが、最後の位置にあると、フィルタリングの効率が低下することを意味し、一つのトレースを取得する際に大量のデータをスキャンする必要があるかもしれません。
OTelコレクタはこの課題に対処するために、マテリアライズドビューと関連するテーブルもインストールします。テーブルとビューは以下のように示されています。
このビューは、テーブルotel_traces_trace_id_ts
がトレースの最小および最大タイムスタンプを持っていることを効果的に保証します。このテーブルはTraceId
で並べ替えられており、これによりこれらのタイムスタンプを効率的に取得できます。これらのタイムスタンプ範囲は、メインのotel_traces
テーブルをクエリするときに使用できます。具体的には、Grafanaが以下のクエリを使用してIDによってトレースを取得します。
CTEはここでトレースID ae9226c78d1d360601e6383928e4d22d
の最小および最大タイムスタンプを特定し、これを使用してメインのotel_traces
を関連するスパンのフィルタリングに使用します。
このアプローチは、同様のアクセスパターンに対しても適用できます。データモデリングの類似した例をこちらで探ります。
プロジェクションを使用する
ClickHouseのプロジェクションを使用すると、テーブルに対して複数のORDER BY
句を指定できます。
以前のセクションでは、マテリアライズドビューがClickHouseで集約を事前計算し、行を変換し、異なるアクセスパターンのために観測性クエリを最適化するためにどのように使用できるかを探討しました。
ここでは、マテリアライズドビューが異なる順序キーを持つターゲットテーブルに行を送信し、トレースIDによる検索の最適化のために、元のテーブルは挿入の順序で受け取る必要があるという例を提供しました。
プロジェクションは同じ問題に対処するために使用でき、ユーザーはプライマリキーの一部でないカラムのクエリの最適化を行うことができます。
理論的には、この機能を使用して、テーブルに対して複数の並びキーを提供できますが、一つの大きな欠点があります:データの重複です。具体的には、データは各プロジェクションに指定された順序とは別に、メインプライマリキーの順序に従って書き込む必要があります。これにより、挿入が遅くなり、ディスクスペースがより多く消費されます。
プロジェクションは、マテリアライズドビューと同様の多くの機能を提供しますが、後者の方が好まれることが多く、使用は控えめにすべきです。ユーザーは欠点を理解し、適切な場合にそれらを使用すべきです。たとえば、プロジェクションは集約を事前計算するために使用できますが、ユーザーはこの目的のためにマテリアライズドビューの使用を推奨します。

以下のクエリを考えてみましょう。それはotel_logs_v2
テーブルを500エラーコードでフィルタリングしています。これは、ユーザーがエラーコードでフィルタリングしたい場合の一般的なアクセスパターンである可能性があります:
ここでFORMAT Null
を使用して結果を出力しません。これにより、すべての結果が読み取られますが、返されないため、LIMITによるクエリの早期終了を防ぎます。これは、全10m行をスキャンするのにかかった時間を示すためだけのものです。
上記のクエリは、選択した並びキー(ServiceName, Timestamp)
に対して線形スキャンを必要とします。上記のクエリのパフォーマンスを改善するために、Status
を並びキーの末尾に追加することもできますが、プロジェクションを追加することもできます。
最初にプロジェクションを作成し、その後に物質化する必要があることに注意してください。この後者のコマンドは、データを二つの異なる順序でディスクに二重に保存することになります。データ作成時にプロジェクションを定義することもできます。以下のように、データが挿入されると自動的に維持されます。
重要なのは、プロジェクションがALTER
を介して作成された場合、その作成は非同期であり、MATERIALIZE PROJECTION
コマンドが発行されたときに実行されます。ユーザーは次のクエリでこの操作の進捗を確認し、is_done=1
を待つことができます。
上記のクエリを繰り返すと、パフォーマンスが大幅に改善されていることがわかりますが、追加のストレージを犠牲にします(詳しくは"テーブルサイズと圧縮の測定"を参照してください)。
上記の例では、プロジェクションで以前のクエリで使用されたカラムを指定しています。これにより、指定されたカラムのみがプロジェクションの一部としてディスクに格納され、Statusで並べ替えられます。代わりにSELECT *
を使用すると、すべてのカラムが保存されます。これにより、任意のカラムの subset を使用してクエリを実行できるようになりますが、追加のストレージが発生します。ディスクスペースと圧縮を測定するには、"テーブルサイズと圧縮の測定"を参照してください。
セカンダリ/データスキッピングインデックス
ClickHouseでプライマリキーがどれほど調整されていても、一部のクエリは必然的にテーブル全体のスキャンを必要とします。これは、マテリアライズドビューを使用することによって緩和できます(そして一部のクエリにはプロジェクションを使用します)が、これらは追加のメンテナンスを必要とし、ユーザーはそれらの使用を確実にするためにその可用性を意識する必要があります。伝統的なリレーショナルデータベースはセカンダリインデックスでこれを解決しますが、これはClickHouseのような列指向データベースでは効果的ではありません。その代わりに、ClickHouseは「スキップ」インデックスを使用し、一致する値のない大きなデータチャンクをスキップできるため、クエリのパフォーマンスを大幅に向上させることができます。
デフォルトのOTelスキーマは、マップアクセスのアクセラレーションを試みるためにセカンダリインデックスを使用しています。これらが一般的に効果がないことがわかり、カスタムスキーマにコピーすることをお勧めしませんが、スキッピングインデックスは依然として便利です。
ユーザーは、これらを適用しようとする前にセカンダリインデックスのガイドを読み、理解しておくべきです。
一般に、プライマリキーとターゲットとする非プライマリカラム/式との間に強い相関がある場合、かつユーザーが希少な値、つまり多くのグラニュールに存在しない値を探している場合に、効果的です。
テキスト検索のためのブルームフィルター
観測性クエリにおいて、テキスト検索を行う必要がある際に、セカンダリインデックスは役立つことがあります。特に、ngramとトークンベースのブルームフィルターインデックスngrambf_v1
とtokenbf_v1
は、LIKE
、IN
、およびhasToken演算子を使用してStringカラムの検索を加速するために使用できます。重要なのは、トークンベースのインデックスが区切り文字として非アルファベット文字を使用してトークンを生成する点です。これにより、クエリ時にトークン(単語そのもの)のみが一致することができます。より詳細なマッチングには、N-グラムブルームフィルターを使用できます。これは、文字列を指定されたサイズのnグラムに分割し、サブワードマッチングを可能にします。
生成されるトークンとしたがって一致するものを評価するために、tokens
関数を使用できます:
ngram
関数は、第二のパラメータとしてnグラムサイズを指定できる同様の機能を提供します。
ClickHouseはまた、セカンダリインデックスとして逆インデックスの実験的なサポートも持っています。現在、これらをログデータセットには推奨していませんが、製品版の準備が整ったらトークンベースのブルームフィルターを置き換えることを予想しています。
この例では構造化されたログデータセットを使用します。Referer
カラムにultra
が含まれるログをカウントしたいとします。
ここで、nグラムサイズが3でマッチする必要があります。そのため、ngrambf_v1
インデックスを作成します。
インデックスngrambf_v1(3, 10000, 3, 7)
はここで4つのパラメータを取ります。これらのうち最後の値(7)はシードを表します。他のものはnグラムサイズ(3)、値m
(フィルタサイズ)、およびハッシュ関数の数k
(7)を表します。k
とm
はチューニングが必要で、ユニークなnグラム/トークンの数やフィルタが真負を返す確率に基づいています。これらの値を確立するためには、これらの関数を推奨します。
適切にチューニングされた場合、ここでの速度向上はかなり重要です:
上記は説明を目的とした例に過ぎません。ユーザーは、テキスト検索を最適化するためにトークンベースのブルームフィルターを使用するのではなく、挿入時にログから構造を抽出することを推奨します。ただし、スタックトレースや構造が不明確な他の大きな文字列がある場合、テキスト検索が役立つことがあります。
ブルームフィルターを使用する際の一般的なガイドライン:
ブルームの目的は、グラニュールをフィルタリングし、カラムのすべての値を読み込む必要を回避することです。EXPLAIN
句で、パラメータindexes=1
を使用してスキップされたグラニュールの数を特定できます。以下は、元のテーブルotel_logs_v2
とnグラムブルームフィルターを持つテーブルotel_logs_bloom
のそれぞれに対するレスポンスです。
ブルームフィルターは通常、カラムよりも小さい場合にのみ速度が向上します。もし大きい場合、性能向上はほとんどないでしょう。フィルターサイズとカラムサイズを比較するには、次のクエリを使用します:
上記の例では、セカンダリブルームフィルターインデックスは12MBで、カラム自体の圧縮サイズの56MBに対してほぼ5倍小さいことがわかります。
ブルームフィルターは大規模な調整が必要です。最適な設定を特定するためのノートはこちらを参照してください。ブルームフィルターは挿入やマージ時にコストがかかる場合もあります。ユーザーは生産環境にブルームフィルターを追加する前に、その影響を評価するべきです。
セカンダリスキップインデックスの詳細はこちらで確認できます。
マップからの抽出
マップ型はOTelスキーマで一般的です。この型は、値とキーが同じ型である必要があります - Kubernetesラベルなどのメタデータには十分です。マップ型のサブキーをクエリする際には、親カラム全体が読み込まれることに注意してください。マップに多くのキーがある場合、これによりディスクから読み取るデータが増えるため、クエリペナルティが大きくなることがあります。
特定のキーを頻繁にクエリする場合は、そのキーをルートの専用カラムに移動することを検討してください。これは通常、一般的なアクセスパターンに応じて、展開後に行われる作業であり、製品化前に予測することが難しい場合があります。展開後にスキーマを変更する方法については"スキーマ変更の管理"を参照してください。
テーブルサイズと圧縮の測定
ClickHouseが観測性のために使用される主な理由の一つは圧縮です。
ストレージコストを劇的に削減するだけでなく、ディスク上のデータが少ないことでI/Oが減り、クエリや挿入がより速くなります。I/Oの削減は、CPUに関する影響を考慮すると、どんな圧縮アルゴリズムのオーバーヘッドも上回るはずです。したがって、データの圧縮を改善することは、ClickHouseのクエリを高速に保つための主要な焦点となるべきです。
圧縮の測定に関する詳細はこちらで確認できます。