一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • ClickHouseで構築する高速分析基盤:OLAPワークロードの最適解

なぜ今ClickHouseなのか

大量のデータを高速に分析したい——この要求は年々高まる一方だ。筆者は過去に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;

ここでのポイントをいくつか挙げる。

  • ORDER BY: クエリのWHERE句で最も頻繁に使われるカラムを先頭に配置する。カーディナリティの低い順に並べるのが基本だ
  • PARTITION BY: 月単位のパーティションは多くの時系列データで適切。日単位にすると小さなパーツが大量にできるため避ける
  • LowCardinality: カーディナリティの低い文字列カラムには必ず適用する。辞書エンコーディングにより大幅に性能が向上する
  • TTL: データ保持期間を設定することで、自動的に古いデータが削除される

マテリアライズドビューによる事前集計

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ワークロードに対して圧倒的なパフォーマンスを発揮する。テーブル設計とクエリパターンの最適化が肝要だが、そのコストに見合うだけの速度とスケーラビリティが得られる。分析基盤の刷新を検討しているなら、ぜひ候補に加えてほしい。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア