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

OLTP + OLAP デュアル構成

ここまでで、列指向ストレージの仕組みClickHouse の基本データ投入パターン を学びました。

このドキュメントでは、OLTP データベース(PostgreSQL 等)と OLAP データベース(ClickHouse 等)を組み合わせた設計パターンを学びます。

idp-server での適用例: このデュアル構成を idp-server のテナント統計機能に適用した場合の詳細は テナント統計機能 実装ガイド を参照してください。


なぜデュアル構成か

1つのデータベースで全てをまかなうと、どこかで限界が来ます。

┌─────────────────────────────────────────────────────────────────┐
│ OLTP DB 1台で全部やる場合 │
│ │
│ 通常のリクエスト処理 年次レポート生成 │
│ (INSERT + SELECT) (数億行 GROUP BY) │
│ │ │ │
│ └──────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ PostgreSQL │ │
│ │ │ ← 通常処理が遅くなる(レポートがIO占有)│
│ │ │ ← レポートも遅い(行指向で非効率) │
│ └──────────────────┘ │
│ │
│ 「リクエスト処理とレポート生成が互いに足を引っ張る」 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ デュアル構成の場合 │
│ │
│ 通常のリクエスト処理 年次レポート生成 │
│ (INSERT + SELECT) (数億行 GROUP BY) │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ ClickHouse │ │
│ │ (OLTP) │ │ (OLAP) │ │
│ │ │ │ │ │
│ │ ・リクエスト 数ms │ │ ・年次集計 数秒 │ │
│ │ ・トランザクション │ │ ・事前集計不要 │ │
│ │ ・更新/削除 │ │ ・圧縮10-40倍 │ │
│ └──────────┬────────┘ └──────────────────┘ │
│ │ CDC (自動同期) ↑ │
│ └──────────────────┘ │
│ │
│ 「それぞれ得意なことに専念」 │
└─────────────────────────────────────────────────────────────────┘

アーキテクチャ全体像

典型的なデュアル構成のアーキテクチャ:

┌─ アプリケーション ──────────────────────────────────────────────────┐
│ │
│ ┌─ OLTP パス(トランザクション処理)────────────────────────────┐ │
│ │ │ │
│ │ リクエスト処理 → ビジネスロジック → イベント記録 │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ PostgreSQL │ │ │
│ │ │ │ │ │
│ │ │ ・ユーザー/セッション/トランザクションデータ │ │ │
│ │ │ ・イベントログ (INSERT) │ │ │
│ │ │ ・マスターデータ │ │ │
│ │ └───────────────────────────────┬──────────────────────┘ │ │
│ │ │ │ │
│ └───────────────────────────────────┼───────────────────────────┘ │
│ │ │
│ │ CDC(イベントログのみ同期) │
│ │ │
│ ┌─ OLAP パス(分析・統計)──────────┼───────────────────────────┐ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ ClickHouse │ │ │
│ │ │ │ │ │
│ │ │ ・イベントログ(全量、列指向、圧縮) │ │ │
│ │ │ ・生データから直接 COUNT / GROUP BY / uniqExact │ │ │
│ │ │ ・事前集計テーブル不要 │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 統計API / ダッシュボード / レポート │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘

何を同期するか

全テーブルを同期する必要はありません。分析対象のテーブルだけ

テーブルの種類同期理由
イベントログ(大量、追記型)分析の主対象。CDC で自動同期
マスターデータ(ユーザー、設定等)JOIN が必要な場合のみ。ClickHouse の Dictionary で参照
トランザクションデータ(セッション、トークン等)OLTP 専用。分析対象外
事前集計テーブルOLAP があれば不要(生データから直接集計)

事前集計テーブルが不要になる理由

OLTP のみの構成では、大量データの集計に耐えられないため事前集計テーブルを用意します。

OLTP のみ:
events テーブル(数億行)→ 直接 COUNT は遅い
→ statistics テーブル(事前集計)を別途用意
→ バッチ処理で定期的に集計
→ ロック競合、TZ問題、テーブル設計の複雑化...

OLTP + OLAP:
events テーブル(数億行)→ ClickHouse にコピー
→ ClickHouse で直接 COUNT / GROUP BY(数秒)
→ 事前集計テーブル不要
→ バッチ処理不要
→ 統計周りのコード・テーブル・運用がまるごと消える

データフローの詳細

書き込みフロー

ユーザー → リクエスト → アプリケーション


PostgreSQL
INSERT INTO events (...)

│ WAL (論理レプリケーション)

CDC ツール (PeerDB 等)
変換 + バッチ化


ClickHouse
INSERT INTO events (...)
(ReplacingMergeTree)

アプリは PostgreSQL にだけ書く。 ClickHouse への同期は CDC ツールが自動で行う。

読み取りフロー

┌─ 分析クエリのルーティング ──────────────────────────────┐
│ │
│ アプリケーション │
│ ┌──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ if (olap が利用可能) { │ │
│ │ // ClickHouse: 生データから直接集計 │ │
│ │ clickHouseJdbc.query(""" │ │
│ │ SELECT toStartOfMonth(created_at) AS month, │ │
│ │ count(*) AS total, │ │
│ │ uniqExact(user_id) AS unique_users │ │
│ │ FROM events │ │
│ │ WHERE tenant_id = ? AND ... │ │
│ │ GROUP BY month │ │
│ │ """); │ │
│ │ } else { │ │
│ │ // PostgreSQL: 事前集計テーブルから取得 │ │
│ │ postgresJdbc.query(""" │ │
│ │ SELECT stat_date, metric, value │ │
│ │ FROM statistics │ │
│ │ WHERE tenant_id = ? AND ... │ │
│ │ """); │ │
│ │ } │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

段階的な導入

一度に全部変える必要はありません。

Step 1: 検証(数分)

ClickHouse の PostgreSQL エンジンで、既存データを直接クエリしてみる。データ移動なし。

-- ClickHouse から PostgreSQL を直接参照
CREATE TABLE pg_events (...)
ENGINE = PostgreSQL('pg-host:5432', 'mydb', 'events', 'user', 'pass');

SELECT type, count(*) FROM pg_events GROUP BY type;

Step 2: アーカイブ分析

古いデータを S3 にエクスポートし、ClickHouse で読み込んで分析。

SELECT type, count(*)
FROM s3('s3://my-bucket/archive/**/*.parquet', ...)
GROUP BY type;

Step 3: CDC でリアルタイム同期

CDC ツールを導入し、イベントログをリアルタイム同期。分析APIの読み取り先を ClickHouse に切り替え。

Step 4: 事前集計テーブルの廃止

ClickHouse で全集計が賄えることを確認後、PostgreSQL 側の事前集計テーブルとバッチ処理を段階的に廃止。


設計上の注意点

テナント分離

PostgreSQL で RLS(Row Level Security)を使っている場合、ClickHouse には RLS がないため、アプリ層で WHERE tenant_id = ? を必ず付ける設計にする。

整合性と遅延

CDC には数秒の遅延がある。分析APIのレスポンスに「最終同期時刻」を含めることで、利用者にデータの鮮度を伝えられる。

障害時のフォールバック

ClickHouse がダウンしても、PostgreSQL の事前集計テーブルが残っていればフォールバック可能。完全廃止は十分な運用実績の後で。

マスターデータの参照

ClickHouse でイベントデータとユーザー名等を結合したい場合、全テーブルを同期するのではなく、Dictionary 機能で PostgreSQL のマスターデータを参照するのが軽量。


次のステップ