前提条件
- clickhouse-local がローカル環境にインストールされていること。開始方法については、clickhouse-local セットアップガイドを参照してください。
作成するもの
OpenTelemetry では、すべてのトレーススパンに リソース属性 のセットが含まれます。これは、テレメトリーを生成した対象 (サービス名、ホスト、クラウドリージョン、Kubernetes ポッドなど) を表すキー・バリューのメタデータです。キーのセットはサービスや環境によって異なるため、これは ClickHouse の Map 型に適しています。キーは動的でアプリケーション固有ですが、通常は 1 行あたり数個しか含まれません。
このクイックスタートでは、clickhouse-local を使って CSVファイル から実際の OTel トレースデータを Map(LowCardinality(String), String) カラムを持つテーブルに読み込み、Map データのクエリ、フィルタリング、集計、最適化の方法を学びます。
サンプルデータをダウンロード
このデータセットには、デモ用マイクロサービスアプリケーションからエクスポートされた 6,120 件の OTel トレーススパンが含まれています。各行には、動的なキー・バリューのペアを JSON map として格納した ResourceAttributes カラムと SpanAttributes カラムが含まれます。
ファイルは、たとえば ~/data/data-otel-traces.csv のように、簡単に参照できるディレクトリに保存してください。
data-otel-traces.csv をダウンロード (2.9 MB)
1 行分のデータは次のようになります。
Timestamp: 2025-12-26 00:00:45.759467000
TraceId: 0da128e6e3c01bc38b6b43a33e5fa522
SpanId: 3774f759424e4006
ParentSpanId: 2fdd1e5b66605098
SpanName: orders receive
SpanKind: SPAN_KIND_CONSUMER
ServiceName: accountingservice
Duration: 5361
StatusCode: STATUS_CODE_UNSET
ResourceAttributes: {"host.name":"f19476836e47","os.type":"linux","process.pid":"1","process.command_args":"[\"./accountingservice\"]","process.executable.path":"...
SpanAttributes: {"network.transport":"tcp","messaging.destination.name":"orders","messaging.kafka.message.offset":"232260","messaging.message.body.size":"216"...テーブルを作成してデータを読み込む
clickhouse-local を起動し、CSV に対応するスキーマで次のテーブルを作成します。
重要なカラムは ResourceAttributes Map(LowCardinality(String), String) です。OTel の属性キーは、比較的少数の同じ値が繰り返し現れるため、キー型に LowCardinality を使用しています。
CREATE TABLE otel_traces
(
Timestamp DateTime64(9),
TraceId String,
SpanId String,
ParentSpanId String,
SpanName LowCardinality(String),
SpanKind LowCardinality(String),
ServiceName LowCardinality(String),
Duration UInt64,
StatusCode LowCardinality(String),
ResourceAttributes Map(LowCardinality(String), String),
SpanAttributes Map(LowCardinality(String), String)
)
ENGINE = MergeTree()
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp));次に、file テーブルエンジンを使って CSV を読み込みます。パスは、ファイルを保存した場所に合わせて調整してください。
INSERT INTO otel_traces
SELECT * FROM file('~/data/data-otel-traces.csv', CSVWithNames);データが読み込まれていることを確認します:
SELECT count() FROM otel_traces;6,120行が表示されるはずです。
データをクエリする
特定のキーにアクセスする — 角括弧構文を使って、map から値を取り出します。行にそのキーが存在しない場合は、値の型の既定値が返されます (String の場合は空文字列) :
SELECT
ServiceName,
SpanName,
ResourceAttributes['host.name'] AS host,
ResourceAttributes['k8s.pod.name'] AS pod,
ResourceAttributes['deployment.environment'] AS env
FROM otel_traces
LIMIT 10;Mapの値でフィルタリング — 特定のサービス名に一致するすべてのスパンを見つけます:
SELECT
Timestamp,
SpanName,
Duration / 1e6 AS duration_ms
FROM otel_traces
WHERE ResourceAttributes['service.name'] = 'cartservice'
ORDER BY Timestamp
LIMIT 10;キーが存在するか確認する — すべてのスパンに Kubernetes のメタデータが付いているわけではありません。mapContains を使うと、どのスパンに含まれているかを確認できます:
SELECT
ServiceName,
SpanName,
mapContains(ResourceAttributes, 'k8s.node.name') AS has_node_info
FROM otel_traces
LIMIT 10;データセット全体で使われているすべてのキーを確認する — どのインストルメンテーションがデータを生成しているかを把握するのに役立ちます。
SELECT DISTINCT arrayJoin(mapKeys(ResourceAttributes)) AS key
FROM otel_traces
ORDER BY key;ARRAY JOINでMapを行に展開 — 各キー・バリューのペアを1行ずつに展開します。属性の一覧を作成したり、ダッシュボードにデータを渡したりするのに便利です。
SELECT
ServiceName,
key,
value
FROM otel_traces
ARRAY JOIN
mapKeys(ResourceAttributes) AS key,
mapValues(ResourceAttributes) AS value
WHERE ServiceName = 'cartservice'
LIMIT 20;mapFilter で Map をフィルタリング — 各スパンから Kubernetes 関連の属性のみを抽出します:
SELECT
ServiceName,
mapFilter((k, v) -> k LIKE 'k8s.%', ResourceAttributes) AS k8s_attrs
FROM otel_traces
WHERE mapContains(ResourceAttributes, 'k8s.pod.name')
LIMIT 10;エラースパンとそのリソースのコンテキストを特定する — 通常のカラムフィルターとマップアクセスを組み合わせます:
SELECT
Timestamp,
ServiceName,
SpanName,
ResourceAttributes['host.name'] AS host,
ResourceAttributes['k8s.pod.name'] AS pod,
SpanAttributes['error.type'] AS error_type,
SpanAttributes['error.message'] AS error_message
FROM otel_traces
WHERE StatusCode = 'STATUS_CODE_ERROR';-Map combinator を使って Map をキーごとに集計する
ClickHouse の -Map 集約コンビネータを使うと、任意の集約関数を Map カラムに適用し、各キーごとに独立して集計できます。結果も Map となり、キーごとに 1 つのエントリと、その集計済みの値が格納されます。これは、カウンターや Gauge が Map の値として保存される OTel メトリクスで特に有用です。
これを確認するために、各行に HTTP ステータスコードごとの件数を Map(String, UInt64) として記録する小さなメトリクス テーブルを作成します。
CREATE TABLE otel_http_status_counts
(
Timestamp DateTime,
ServiceName LowCardinality(String),
StatusCounts Map(String, UInt64)
)
ENGINE = MergeTree()
ORDER BY (ServiceName, Timestamp);
INSERT INTO otel_http_status_counts VALUES
('2025-12-26 10:00:00', 'cart-service', {'2xx': 150, '4xx': 12, '5xx': 3}),
('2025-12-26 10:01:00', 'cart-service', {'2xx': 200, '4xx': 8, '5xx': 1}),
('2025-12-26 10:00:00', 'inventory-service', {'2xx': 90, '4xx': 5}),
('2025-12-26 10:01:00', 'inventory-service', {'2xx': 110, '4xx': 3, '5xx': 2}),
('2025-12-26 10:00:00', 'payment-service', {'2xx': 50, '5xx': 10}),
('2025-12-26 10:01:00', 'payment-service', {'2xx': 45, '4xx': 2, '5xx': 15});次に、sumMap を使って、各サービスについてステータスコードごとの件数を合計します。
SELECT
ServiceName,
sumMap(StatusCounts) AS total_by_status
FROM otel_http_status_counts
GROUP BY ServiceName;-Map 接尾辞は任意の集約関数と組み合わせて使えるため、minMap、maxMap、avgMap も同様に簡単に使えます:
SELECT
ServiceName,
avgMap(StatusCounts) AS avg_by_status,
maxMap(StatusCounts) AS peak_by_status
FROM otel_http_status_counts
GROUP BY ServiceName;これを他の combinator と組み合わせることもできます。たとえば、sumMapIf を使うと条件付きで集計できます。ここでは、サービスですでにエラーが発生していた分単位のウィンドウだけを合計します。
SELECT
ServiceName,
sumMapIf(StatusCounts, StatusCounts['5xx'] > 0) AS totals_in_error_windows
FROM otel_http_status_counts
GROUP BY ServiceName;OTel でこれが重要な理由: OTel collector が 1 分ごとのステータスコードの内訳を ClickHouse に書き込む場合、sumMap を使えば、それらを 1 回のクエリで時間単位や日単位の totals に集計できます。ARRAY JOIN も unpivot も不要で、キーの全体集合を事前に把握しておく必要もありません。どの行に現れたキーも、自動的に結果に含まれます。
頻繁にクエリするキー向けに最適化する
同じ Map キーで繰り返し絞り込みを行う場合 — host.name はその代表例です — それをマテリアライズドカラムとして抽出できます。こうすることで、クエリのたびに Map 全体を線形走査せずに済みます。
ALTER TABLE otel_traces
ADD COLUMN HostName String
MATERIALIZED ResourceAttributes['host.name'];既存データに対しては、カラムをバックフィルします:
ALTER TABLE otel_traces MATERIALIZE COLUMN HostName;これで WHERE HostName = 'prod-cart-01' は、マップ全体ではなく、そのために用意された単一のカラムだけを読み取るようになります。これは、頻繁にクエリするあらゆる attribute について、OTel の ClickHouse スキーマで推奨されるパターンです。
要点
Map(LowCardinality(String), String)は OTel の属性に適した定番の型です。キー集合が変化しても柔軟に扱え、LowCardinalityによってキーの保存効率も高く保てます。- ブラケット構文 (
map['key']) は値にアクセスする最も一般的な方法ですが、線形走査になる点には注意してください。キーが数十個の map であれば問題ありませんが、数百個になると最適とはいえません。 - マテリアライズドカラム は有効な手段です。map のキーが頻繁にフィルタ条件の対象になる場合は、それを実カラムに昇格させることで、索引付きの列指向アクセスが可能になります。
mapContains,mapKeys,mapValues,mapFilterとARRAY JOINを使えば、SQL の中だけで map データを調べたり変換したりできる、豊富な手段が得られます。-Mapaggregate combinator (sumMap,avgMap,maxMapなど) は、行をまたいで各キーを個別に集計します。キー集合を事前に把握していなくても OTel メトリクスのカウンターを集約するのに最適で、ほかの集約関数コンビネータと組み合わせることもできます (たとえばsumMapIf) 。
次のステップ
次は、以下のクイックスタートをご覧ください。
また、リファレンスドキュメントでさらに詳しく確認することもできます。
