大量のデータを高速に分析したい——この要求は年々高まる一方だ。筆者は過去にBigQuery、Redshift、Prestoなどさまざまな分析基盤を構築・運用してきたが、ClickHouseは特にコストパフォーマンスとクエリ速度の面で際立った選択肢となっている。
ClickHouseはYandex社が開発したオープンソースの列指向データベースだ。OLAPワークロードに特化して設計されており、数十億行のデータに対するアグリゲーションクエリを秒単位で返す能力を持つ。商用のクラウドDWHと比較して、セルフホスティングであれば大幅なコスト削減が可能だ。
ClickHouseの速度の根幹は列指向ストレージにある。従来のRDBMS(行指向)では1行のデータがまとめて保存されるが、ClickHouseでは各カラムが独立して保存される。これにより、分析クエリで参照するカラムのみをディスクから読み出せるため、I/Oが劇的に削減される。
-- 例:10億行のアクセスログから日別PVを集計
SELECT
toDate(timestamp) AS date,
count() AS page_views
FROM access_logs
WHERE timestamp >= '2025-01-01'
GROUP BY date
ORDER BY date;
-- 行指向DB: 全カラムを読む必要あり → 遅い
-- ClickHouse: timestamp カラムのみ読む → 高速
さらに、同じカラムには同じ型のデータが並ぶため、圧縮効率が非常に高い。実測では生データの10分の1から20分の1程度に圧縮されることが多い。ストレージコストの面でも有利だ。
ClickHouseで最も重要な設計判断はテーブルエンジンとソートキーの選択だ。分析用途ではMergeTreeファミリーを使う。
CREATE TABLE events (
event_id UUID,
user_id UInt64,
event_type LowCardinality(String),
properties String, -- JSON文字列
timestamp DateTime,
date Date MATERIALIZED toDate(timestamp)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (event_type, user_id, timestamp)
TTL timestamp + INTERVAL 2 YEAR DELETE
SETTINGS index_granularity = 8192;
ここでのポイントをいくつか挙げる。
ClickHouseの強力な機能の一つがマテリアライズドビューだ。データ挿入時にリアルタイムで集計結果を別テーブルに書き込むことができる。
-- 日別・イベント種別ごとの集計テーブル
CREATE TABLE daily_event_stats (
date Date,
event_type LowCardinality(String),
event_count AggregateFunction(count),
unique_users AggregateFunction(uniq, UInt64)
) ENGINE = AggregatingMergeTree()
ORDER BY (date, event_type);
-- マテリアライズドビュー
CREATE MATERIALIZED VIEW daily_event_stats_mv
TO daily_event_stats
AS SELECT
toDate(timestamp) AS date,
event_type,
countState() AS event_count,
uniqState(user_id) AS unique_users
FROM events
GROUP BY date, event_type;
-- 集計結果のクエリ
SELECT
date,
event_type,
countMerge(event_count) AS total_events,
uniqMerge(unique_users) AS unique_users
FROM daily_event_stats
WHERE date >= today() - 30
GROUP BY date, event_type;
この仕組みにより、ダッシュボードで頻繁に実行される集計クエリを事前計算しておける。10億行のテーブルをスキャンする代わりに、数千行の集計テーブルを参照するだけで済む。レスポンスタイムは数秒から数ミリ秒に短縮される。
本番環境で半年以上運用して得た知見を共有する。
まず、バッチインサートの重要性だ。ClickHouseは大量の小さなINSERTに弱い。1行ずつINSERTすると性能が著しく劣化する。最低でも1000行、理想的には数万行単位でバッチ挿入すべきだ。アプリケーション側でバッファリングするか、Kafkaエンジンを使ってストリーミング取り込みを行うのが実用的だ。
-- Kafkaテーブルによるストリーミング取り込み
CREATE TABLE events_kafka (
event_id UUID,
user_id UInt64,
event_type String,
properties String,
timestamp DateTime
) ENGINE = Kafka
SETTINGS
kafka_broker_list = 'kafka:9092',
kafka_topic_list = 'events',
kafka_group_name = 'clickhouse_consumer',
kafka_format = 'JSONEachRow';
次に、JOINの使い方だ。ClickHouseのJOINは右テーブルをメモリに載せる仕組みのため、右テーブルが大きいとメモリ不足になる。大規模テーブル同士のJOINが必要な場合は、事前に非正規化してフラットなテーブルを作るほうが効率的だ。
本番環境では複数ノードによるクラスタ構成が推奨される。ClickHouseはReplicatedMergeTreeエンジンとClickHouse Keeperを使ったレプリケーションに対応している。シャーディングによる水平スケーリングも可能だが、まずはレプリケーションによる可用性確保を優先すべきだ。
筆者の環境では3ノード構成(2シャード、各シャード2レプリカ、1ノードは両シャードのレプリカを兼任)で運用しており、1日あたり約50億イベントを処理している。ノード障害時も自動フェイルオーバーで可用性が維持される。
ClickHouseはOLAPワークロードに対して圧倒的なパフォーマンスを発揮する。テーブル設計とクエリパターンの最適化が肝要だが、そのコストに見合うだけの速度とスケーラビリティが得られる。分析基盤の刷新を検討しているなら、ぜひ候補に加えてほしい。