メインコンテンツまでスキップ

ClickHouse 入門

このドキュメントは PostgreSQL の学習コンテンツ(特にテーブル設計トランザクションクエリ最適化)を学んだ方を対象に、ClickHouse が PostgreSQL とどう違うかを対比しながら解説します。

ClickHouse とは

オープンソースの列指向OLAPデータベース。大量データの集計・分析に特化。

┌──────────────────────────────────────────────────────────┐
│ ClickHouse の位置づけ │
│ │
│ 書き込み重視 ← バランス → 分析重視 │
│ │ │ │
│ │ PostgreSQL MySQL ClickHouse │ │
│ │ ├ ACID保証 ├ ACID保証 ├ 列指向 │ │
│ │ ├ 行指向 ├ 行指向 ├ 高圧縮 │ │
│ │ └ OLTP └ OLTP └ OLAP │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ClickHouse は「書き込みは追記のみ、分析は超高速」 │
│ PostgreSQL は「何でもできるが、大量集計は苦手」 │
│ → 組み合わせて使うのが最適解 │
│ │
└──────────────────────────────────────────────────────────┘

アーキテクチャ

PostgreSQL は「1つのプロセスが接続を受けて、共有バッファからデータを読む」シンプルなアーキテクチャ(PostgreSQL 概要 参照)。ClickHouse は分析に特化した設計です。

┌─ ClickHouse Server ──────────────────────────────────────┐
│ │
│ ┌──────────────────┐ │
│ │ SQL パーサー │ ← 標準SQL + 拡張関数 │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ クエリプランナー │ ← ベクトル化実行計画 │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 実行エンジン │ ← マルチコア並列 + SIMD │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ MergeTree │ ← 列指向ストレージエンジン │
│ │ ストレージ │ │
│ │ ┌──────────────┐ │ │
│ │ │ パート(chunk) │ │ ← INSERT ごとにパート生成 │
│ │ │ パート(chunk) │ │ バックグラウンドでマージ │
│ │ │ パート(chunk) │ │ │
│ │ └──────────────┘ │ │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘

テーブルエンジン

PostgreSQL ではすべてのテーブルが heap ストレージ + B-tree インデックスで統一されています(インデックス 参照)。ClickHouse はテーブルごとにストレージエンジンを選択する設計です。

MergeTree(基本)

PostgreSQL と同じテーブルを ClickHouse で作るとこうなります。

-- PostgreSQL
CREATE TABLE security_event (
id UUID PRIMARY KEY, -- B-tree インデックス
type VARCHAR(255) NOT NULL,
tenant_id UUID NOT NULL,
user_id UUID,
detail JSONB NOT NULL,
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at); -- パーティション
CREATE INDEX idx_tenant ON security_event (tenant_id, type, created_at);

-- ClickHouse
CREATE TABLE security_event (
id UUID,
type String,
tenant_id UUID,
user_id Nullable(UUID),
detail String,
created_at DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(created_at) -- 日次パーティション
ORDER BY (tenant_id, type, created_at) -- ソートキー ≈ PG のインデックス
TTL created_at + INTERVAL 1 YEAR; -- 自動削除(PGにはない)

PostgreSQL との違い:

概念PostgreSQLClickHouse
PRIMARY KEYB-tree インデックス + 一意制約なし(一意制約がない)
インデックスB-tree, GIN, GiST 等を別途作成ORDER BY がインデックスの役割
パーティションPARTITION BY RANGE + pg_partmanPARTITION BY で組み込み
データ保持期間pg_partman の retention で管理TTL で自動削除(DDLに組み込み)
NULL通常のカラムNullable() で明示指定
JSONJSONB(インデックス可能)String(分析時にJSON関数で抽出)

ReplacingMergeTree(CDC用)

-- CDC(Change Data Capture)で使用
-- 同じキーの行が複数回来ても、最新版だけ残す
CREATE TABLE security_event (
...
)
ENGINE = ReplacingMergeTree(created_at) -- created_at が最新の行を残す
ORDER BY (id);

SQL: PostgreSQL との比較

基本は同じ

-- PostgreSQL でも ClickHouse でも同じ
SELECT tenant_id, type, COUNT(*)
FROM security_event
WHERE created_at >= '2026-01-01'
GROUP BY tenant_id, type
ORDER BY COUNT(*) DESC
LIMIT 10;

ClickHouse 独自の便利関数

-- 条件付きカウント
-- PostgreSQL: COUNT(*) FILTER (WHERE type = 'login_success')
-- ClickHouse:
SELECT
tenant_id,
countIf(type = 'login_success') AS ok,
countIf(type = 'login_failure') AS ng

-- ユニークカウント
-- PostgreSQL: COUNT(DISTINCT user_id)
-- ClickHouse:
SELECT uniqExact(user_id) AS dau

-- 時間丸め
-- PostgreSQL: DATE_TRUNC('month', created_at)
-- ClickHouse:
SELECT toStartOfMonth(created_at) AS month

-- パーセンタイル
-- PostgreSQL: PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time)
-- ClickHouse:
SELECT quantile(0.95)(response_time) AS p95

実践クエリ例

-- テナント別の月次レポート(18億行でも数秒)
SELECT
tenant_id,
toStartOfMonth(created_at) AS month,
countIf(type = 'login_success') AS login_ok,
countIf(type = 'login_failure') AS login_ng,
round(
countIf(type = 'login_failure') * 100.0
/ countIf(type IN ('login_success', 'login_failure')),
2
) AS failure_rate_pct,
uniqExact(user_id) AS mau,
uniqExactIf(user_id, type = 'login_success') AS login_users
FROM security_event
WHERE created_at >= '2025-04-01'
AND created_at < '2026-04-01'
GROUP BY tenant_id, month
ORDER BY tenant_id, month;

INSERT のベストプラクティス

PostgreSQL では1行ずつ INSERT しても問題ありません(WAL に追記、heap に格納)。ClickHouse は列指向のため、INSERT のたびに「パート」と呼ばれるデータチャンクが生成され、バックグラウンドでマージされます。小さなパートが大量にできるとマージが追いつかなくなるため、バッチ INSERT が必須です。

PostgreSQL: 1行 INSERT × 10万回 → 問題なし
ClickHouse: 1行 INSERT × 10万回 → パート10万個 → マージ地獄

❌ 1行ずつ INSERT(遅い)
INSERT INTO t VALUES (1, 'a');
INSERT INTO t VALUES (2, 'b');
INSERT INTO t VALUES (3, 'c');
→ 毎回パートが生成される → マージ負荷大

✅ バッチ INSERT(推奨: 1万行以上をまとめて)
INSERT INTO t VALUES (1, 'a'), (2, 'b'), (3, 'c'), ...;
→ 1パート生成 → 効率的

✅ Async Insert(クライアントがバッチできない場合)
SET async_insert = 1;
INSERT INTO t VALUES (1, 'a');
→ ClickHouse 側でバッファリングしてバッチ化

CDC(PeerDB等)を使う場合: CDC ツールが自動でバッチ化してくれるため、アプリ側でバッチを意識する必要はありません。


PostgreSQL との使い分け

┌───────────────────────────────────────────────────┐
│ 適材適所の判断フロー │
│ │
│ このクエリは... │
│ ├── 1行の読み書き? → PostgreSQL │
│ ├── トランザクションが必要? → PostgreSQL │
│ ├── 100万行以上の集計? → ClickHouse │
│ ├── 自由なアドホック分析? → ClickHouse │
│ └── ダッシュボードの定期更新? → ClickHouse │
│ │
└───────────────────────────────────────────────────┘

次のステップ