Docker環境でのClickHouse構築:セットアップと高性能OLAPクエリの最適化

Database tutorial - IT technology blog
Database tutorial - IT technology blog

クイックスタート(5分):Docker ComposeでClickHouseを立ち上げる

PostgreSQLやMySQLで5,000万行にわたるCOUNT(DISTINCT user_id)のような標準的な集計クエリを実行すると、CPU使用率が100%に張り付いたまま20〜40秒かかることがよくあります。ClickHouseなら、まったく同じクエリを15ミリ秒未満で処理できます。行単位ではなく列単位でデータを保持し、類似したデータ型をまとめて最大90%圧縮した上で、クエリで要求された特定の列のみをスキャンすることで、この驚異的な処理速度を実現しています。

まずはプロジェクト用ディレクトリとdocker-compose.ymlファイルを作成しましょう:

mkdir clickhouse-docker && cd clickhouse-docker
touch docker-compose.yml

docker-compose.ymlに以下の設定を記述します。この構成では、HTTPトラフィック(RESTクライアント、Grafana、Webダッシュボードなど)用にポート8123を、ネイティブTCP接続用にポート9000を公開します:

version: '3.8'

services:
  clickhouse:
    image: clickhouse/clickhouse-server:24.3
    container_name: clickhouse-server
    ports:
      - "8123:8123"
      - "9000:9000"
    environment:
      CLICKHOUSE_DB: analytics
      CLICKHOUSE_USER: analyst
      CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
      CLICKHOUSE_PASSWORD: StrongPassword123!
    volumes:
      - ./clickhouse_data:/var/lib/clickhouse/
      - ./clickhouse_logs:/var/log/clickhouse-server/
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    restart: unless-stopped

バックグラウンド(デタッチモード)でコンテナを起動します:

docker compose up -d

コンテナが正常に起動したら、対話型クライアントセッションに接続します:

docker exec -it clickhouse-server clickhouse-client -u analyst --password StrongPassword123!

エンジンのバージョンと現在のアクティブなデータベースを確認して、インストールをテストします:

SELECT version(), currentDatabase();

詳細解説:テーブルエンジンとデータ取り込み

ClickHouseは専用のストレージエンジンを活用します。本番環境の分析基盤のほぼすべてでMergeTreeファミリーが採用されています。データが書き込まれると、ClickHouseはソート済みのデータパート(Data Part)をディスクに書き出し、バックグラウンドで非同期にマージすることで、データの高効率な圧縮と高速なインデックススキャンを両立します。

MergeTreeスキーマの設計

Webプラットフォーム上のユーザーインタラクションを追跡するイベントテーブルを例に考えてみましょう。ソートキーは、最も頻繁に使用されるクエリのフィルタ条件と直接一致させます:

CREATE TABLE analytics.page_views (
    event_date Date,
    event_timestamp DateTime,
    user_id UInt64,
    url LowCardinality(String),
    referrer LowCardinality(String),
    device_type LowCardinality(String),
    country LowCardinality(FixedString(2)),
    duration_seconds UInt16
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, country, user_id)
SETTINGS index_granularity = 8192;

主要なアーキテクチャ上の設計ポイント

  • LowCardinality(T): 重複の多い文字列をコンパクトな数値ディクショナリキーに置き換えます。ユニーク数が10,000未満のフィールド(country、device_type、HTTPステータスコードなど)に適用すると、ディスク使用量とメモリ消費量を60%〜80%削減できます。
  • PARTITION BY: レコードをディスク上の月別ディレクトリ(例: 202610_1_1_0)に分離します。クエリがevent_date >= '2026-10-01'でフィルタリングする場合、ClickHouseは過去の月別フォルダの読み取りを完全にスキップします。なお、パーティションの総数が1,000を超えるとマージ処理のパフォーマンスが低下するため、時間単位や日単位でのパーティション分割は避けてください。
  • ORDER BY: 疎インデックス(Sparse Primary Index)を構築します。ClickHouseは8,192行(デフォルトのindex_granularity)ごとにインデックスマークを書き出します。1億行分のインデックス全体でも数メガバイトのRAMしか消費しないため、範囲スキャン時に目的のブロックをミリ秒未満で特定できます。

HTTPおよびネイティブツール経由でのデータインポート

ClickHouseはCSV、TSV、Parquet、JSONフォーマットの解析をネイティブでサポートしています。JSONストリーミング用にアドホックなCSVファイルを準備する際は、toolcraft.app/ja/tools/data/csv-to-jsonを使用してローカルで変換できます。ブラウザのクライアントサイドで動作するため、機密性の高い運用データがマシン外に出ることはありません。

curlを使用して改行区切りJSON(NDJSON)をClickHouseに直接ストリーミングするには、次のように実行します:

curl -s -u "analyst:StrongPassword123!" -X POST "http://localhost:8123/?query=INSERT+INTO+analytics.page_views+FORMAT+JSONEachRow" \
  --data-binary '{"event_date":"2026-10-01","event_timestamp":"2026-10-01 10:00:00","user_id":1024,"url":"/pricing","referrer":"google.com","device_type":"desktop","country":"US","duration_seconds":45}
{"event_date":"2026-10-01","event_timestamp":"2026-10-01 10:05:12","user_id":2048,"url":"/docs","referrer":"direct","device_type":"mobile","country":"VN","duration_seconds":120}'

高度な活用法:Materialized Viewとインフライト集計

トラフィックの急増時に、リアルタイムの集計値を計算するために5億件の未加工イベントをスキャンすると、300〜800ミリ秒かかることがあります。ClickHouseのMaterialized View(マテリアライズドビュー)を使用すれば、このレイテンシを排除できます。これはINSERTトリガーのように機能し、書き込み時にデータを集計してサマリー行を関連テーブルに格納します。

リアルタイムメトリクスの事前集計

集計メトリクスを保持するために、SummingMergeTreeを使用したターゲットテーブルを作成します:

CREATE TABLE analytics.daily_country_stats (
    event_date Date,
    country LowCardinality(FixedString(2)),
    total_views UInt64,
    total_duration UInt64
)
ENGINE = SummingMergeTree()
ORDER BY (event_date, country);

CREATE MATERIALIZED VIEW analytics.mv_daily_country_stats
TO analytics.daily_country_stats AS
SELECT
    event_date,
    country,
    count() AS total_views,
    sum(duration_seconds) AS total_duration
FROM analytics.page_views
GROUP BY event_date, country;

page_viewsに行を挿入するたびに、Materialized Viewがそれらの受信バッチを処理し、daily_country_statsのカウンターを加算します。この集計済みテーブルに対するクエリは、わずか2〜4ミリ秒で結果を返します:

SELECT 
    country,
    sum(total_views) AS views,
    round(sum(total_duration) / sum(total_views), 2) AS avg_duration_sec
FROM analytics.daily_country_stats
WHERE event_date >= today() - 7
GROUP BY country
ORDER BY views DESC
LIMIT 5;

OLAPワークロード向けの本番運用ガイドライン

1. 必ずINSERTをバッチ化する

データは常に10,000〜100,000行単位のバッチで送信してください。ClickHouseはINSERTステートメントごとに個別の物理パートディレクトリを作成します。1秒間に数百回も単一行の挿入を行うと、すぐにDB::Exception: Too many parts (300) in all data parts in tableエラーが発生します。取り込みサービスがイベントを1件ずつ受信する場合は、宛先テーブルの手前に中間バッファ(Apache Kafka、Vector、またはClickHouseのBufferテーブルエンジンなど)を配置してください。

2. フィルタの選択性に基づいて主キーの順序を決める

ORDER BY句は、等価フィルタで最も頻繁に使用される列を先頭に配置し、カーディナリティの低い順から高い順へ並ぶように構成します。(event_date, country, user_id)のように定義されたインデックスにより、ClickHouseは日付や国でフィルタリングする際にデータを効率的にスキップできます。user_idのような高カーディナリティなフィールドを先頭に配置すると、日付範囲によるインデックスのプルーニング(不要データの刈り込み)が効かなくなります。

3. ミューテーションは負荷の高い管理タスクとして扱う

ClickHouseはトランザクション的な更新向けには作られていません。ALTER TABLE ... UPDATEやDELETEを実行すると非同期ミューテーションがトリガーされ、ディスク上のデータパート全体が書き直されるため、大量のI/O帯域幅を消費します。レコード状態の更新や重複排除のトラッキングを行う場合は、直接的なSQLミューテーションではなく、ReplacingMergeTreeまたはCollapsingMergeTreeを実装してください。

4. 高並行性のためにDockerのulimitsを設定する

複数のスレッドにまたがって多数の列を対象とする単一の複雑なクエリでも、同時に数百のファイルディスクリプタをオープンすることがあります。Docker Composeでnofileを262144に設定しておくことで、重い分析負荷の下でもToo many open filesによるサーバープロセスのクラッシュを防ぐことができます。

Share: